[gjs/wip/coverage-2: 3/9] Add GjsDebugHooks



commit 508f954a25718b2923ae38fef9ec193f2bbc537d
Author: Sam Spilsbury <smspillaz gmail com>
Date:   Thu Dec 26 08:48:55 2013 -0800

    Add GjsDebugHooks
    
    This object serves as a central point for clients to connect
    to SpiderMonkey's internal debugging functions without stepping on each other.
    
    Through GjsDebugHooks, clients are able to connect to the general
    execution hooks (such as entry and exit of new functions and source files),
    enable single-step mode and recieve interrupts and add arbitrary breakpoints
    on source files.
    
    Each of these functions takes a callback and returns a guint.
    
    Remove the debugging hook with the relevant _remove function.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=721246

 Makefile-test.am              |    3 +-
 Makefile.am                   |    3 +
 gjs/context.cpp               |   13 +
 gjs/context.h                 |    3 +
 gjs/debug-hooks-private.h     |   34 +
 gjs/debug-hooks.cpp           | 1489 +++++++++++++++++++++++++++++++++++++++++
 gjs/debug-hooks.h             |  167 +++++
 test/gjs-test-debug-hooks.cpp |  875 ++++++++++++++++++++++++
 test/gjs-tests-add-funcs.h    |    1 +
 test/gjs-tests.cpp            |    1 +
 10 files changed, 2588 insertions(+), 1 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index 895ebfc..9d6902f 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -23,7 +23,8 @@ gjs_tests_LDADD =             \
 
 gjs_tests_SOURCES =            \
        test/gjs-tests.cpp \
-       test/gjs-test-reflected-script.cpp
+       test/gjs-test-reflected-script.cpp \
+       test/gjs-test-debug-hooks.cpp 
 
 check-local: gjs-tests
        @test -z "${TEST_PROGS}" || ${GTESTER} --verbose ${TEST_PROGS} ${TEST_PROGS_OPTIONS}
diff --git a/Makefile.am b/Makefile.am
index 3028ead..3b870c2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -37,6 +37,7 @@ nobase_gjs_module_include_HEADERS =   \
        gjs/mem.h               \
        gjs/native.h    \
        gjs/reflected-script.h \
+       gjs/debug-hooks.h \
        gi/ns.h         \
        gi/object.h     \
        gi/foreign.h    \
@@ -57,6 +58,7 @@ nobase_gjs_module_include_HEADERS =   \
 noinst_HEADERS +=              \
        gjs/jsapi-private.h     \
        gjs/reflected-script-private.h \
+       gjs/debug-hooks-private.h \
        gi/proxyutils.h         \
        util/crash.h            \
        util/hash-x32.h         \
@@ -109,6 +111,7 @@ libgjs_la_SOURCES =         \
        gjs/gi.h                \
        gjs/gi.cpp              \
        gjs/reflected-script.cpp \
+       gjs/debug-hooks.cpp \
        gjs/jsapi-private.cpp   \
        gjs/jsapi-util.cpp      \
        gjs/jsapi-dynamic-class.cpp \
diff --git a/gjs/context.cpp b/gjs/context.cpp
index bf2fb7a..68f3bc6 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -26,6 +26,7 @@
 #include <gio/gio.h>
 
 #include "context.h"
+#include "debug-hooks.h"
 #include "importer.h"
 #include "jsapi-util.h"
 #include "native.h"
@@ -62,6 +63,8 @@ struct _GjsContext {
     JSContext *context;
     JSObject *global;
 
+    GjsDebugHooks *hooks;
+
     char *program_name;
 
     char **search_path;
@@ -327,6 +330,8 @@ gjs_context_dispose(GObject *object)
     GjsContext *js_context;
 
     js_context = GJS_CONTEXT(object);
+    
+    g_clear_object(&js_context->hooks);
 
     if (js_context->global != NULL) {
         js_context->global = NULL;
@@ -441,6 +446,8 @@ gjs_context_constructed(GObject *object)
                                   js_context->global))
         g_error("Failed to point 'imports' property at root importer");
 
+    js_context->hooks = gjs_debug_hooks_new(js_context);
+
     JS_EndRequest(js_context->context);
 
     g_mutex_lock (&contexts_lock);
@@ -739,3 +746,9 @@ gjs_object_get_property_const(JSContext      *context,
     pname = gjs_context_get_const_string(context, property_name);
     return JS_GetPropertyById(context, obj, pname, value_p);
 }
+
+GjsDebugHooks *
+gjs_context_get_debug_hooks(GjsContext *js_context)
+{
+    return js_context->hooks;
+}
diff --git a/gjs/context.h b/gjs/context.h
index 372f309..c641383 100644
--- a/gjs/context.h
+++ b/gjs/context.h
@@ -35,6 +35,8 @@ G_BEGIN_DECLS
 typedef struct _GjsContext      GjsContext;
 typedef struct _GjsContextClass GjsContextClass;
 
+typedef struct _GjsDebugHooks   GjsDebugHooks;
+
 #define GJS_TYPE_CONTEXT              (gjs_context_get_type ())
 #define GJS_CONTEXT(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), GJS_TYPE_CONTEXT, GjsContext))
 #define GJS_CONTEXT_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST ((klass), GJS_TYPE_CONTEXT, GjsContextClass))
@@ -61,6 +63,7 @@ gboolean        gjs_context_define_string_array  (GjsContext  *js_context,
                                                   gssize         array_length,
                                                   const char   **array_values,
                                                   GError       **error);
+GjsDebugHooks*  gjs_context_get_debug_hooks      (GjsContext *js_context);
 
 GList*          gjs_context_get_all              (void);
 
diff --git a/gjs/debug-hooks-private.h b/gjs/debug-hooks-private.h
new file mode 100644
index 0000000..015a952
--- /dev/null
+++ b/gjs/debug-hooks-private.h
@@ -0,0 +1,34 @@
+/*
+ * 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_HOOKS_PRIVATE_H
+#define GJS_DEBUG_HOOKS_PRIVATE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+struct _GjsDebugScriptInfo {
+    const char         *filename;
+    unsigned int       begin_line;
+};
+
+G_END_DECLS
+
+#endif
diff --git a/gjs/debug-hooks.cpp b/gjs/debug-hooks.cpp
new file mode 100644
index 0000000..84aef6b
--- /dev/null
+++ b/gjs/debug-hooks.cpp
@@ -0,0 +1,1489 @@
+/*
+ * 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 <glib-object.h>
+
+#include <gjs/gjs.h>
+#include <gjs/compat.h>
+#include <gjs/gjs-module.h>
+
+#include <gjs/debug-hooks.h>
+#include <gjs/reflected-script.h>
+
+#include "debug-hooks-private.h"
+
+struct _GjsDebugHooksPrivate {
+    /* Non-owning reference to the context */
+    GjsContext *context;
+
+    /* Hook usage counts.
+     *
+     * Each of these counters correspond to a particular function
+     * that we have a registered callback for or need for in SpiderMonkey.
+     *
+     * When someone wants to use the function we increment the count
+     * and if they are the first user, set it up to be in the right state.
+     * When someone is the last user and no longer wants to use the
+     * function, they decrement the count and then do appropriate
+     * tear down on the state.
+     *
+     * There are states that we absolutely do not want to leave enabled
+     * longer than we have to, for instance, JS_SetSingleStepMode or
+     * JS_SetDebugMode */
+    unsigned int debug_mode_usage_count;
+    unsigned int single_step_mode_usage_count;
+    unsigned int interrupt_function_usage_count;
+    unsigned int call_and_execute_hook_usage_count;
+    unsigned int new_script_hook_usage_count;
+
+    /* These are data structures which contain callback points
+     * whenever our internal JS debugger hooks get called */
+
+    /* Breakpoints are those which have been activated in the context
+     * and have a trap set on them. Pending breakpoints are those for
+     * scripts that we haven't loaded yet and will be activated as
+     * soon as they are loaded. A breakpoint's callback will
+     * be triggered if we hit that particular breakpoint */
+    GHashTable *breakpoints;
+    GHashTable *pending_breakpoints;
+
+    /* Each of these are all called as soon as we single-step
+     * one line, enter a new execution frame, or load a new script */
+    GArray     *single_step_hooks;
+    GArray     *call_and_execute_hooks;
+    GArray     *new_script_hooks;
+
+    /* These are data structures which we can use to
+     * look up the keys for the above structures on
+     * destruction. */
+    GHashTable *breakpoints_connections;
+    GHashTable *single_step_connections;
+    GHashTable *call_and_execute_connections;
+    GHashTable *new_script_connections;
+
+    /* This is a hashtable of GjsDebugScriptLookupInfo to
+     * JSScript */
+    GHashTable *scripts_loaded;
+
+    /* An array of jsbytecode for the stack of program counters
+     * as we have entered/exited from our execution hook. We push
+     * a new program counter on to the stack every time we enter a
+     * new frame and can get this information on frame exit to
+     * determine the location of each function in a stack */
+    GArray *pc_stack;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GjsDebugHooks,
+                           gjs_debug_hooks,
+                           G_TYPE_OBJECT)
+
+enum {
+    PROP_0,
+    PROP_CONTEXT,
+    PROP_N
+};
+
+static GParamSpec *properties[PROP_N];
+
+typedef struct _GjsLocationInfo {
+    GjsFrameInfo current_frame;
+
+    /* Storage for filenames and function names so
+     * that we don't expose non-const memory to
+     * users of GjsFunctionKey */
+    char *filename;
+    char *current_function_name;
+} GjsLocationInfo;
+
+const GjsFrameInfo *
+gjs_location_info_get_current_frame(const GjsLocationInfo *info)
+{
+    return &info->current_frame;
+}
+
+const char *
+gjs_debug_script_info_get_filename(const GjsDebugScriptInfo *info)
+{
+    return (const char *) info->filename;
+}
+
+typedef struct _GjsDebugUserCallback {
+    GCallback callback;
+    gpointer  user_data;
+} GjsDebugUserCallback;
+
+static void gjs_debug_hooks_finish_using_new_script_callback(GjsDebugHooks *hooks);
+
+static void
+gjs_debug_user_callback_assign(GjsDebugUserCallback *user_callback,
+                               GCallback             callback,
+                               gpointer              user_data)
+{
+    user_callback->callback = callback;
+    user_callback->user_data = user_data;
+}
+
+static GjsDebugUserCallback *
+gjs_debug_user_callback_new(GCallback callback,
+                            gpointer  user_data)
+{
+    GjsDebugUserCallback *user_callback = g_new0(GjsDebugUserCallback, 1);
+    gjs_debug_user_callback_assign(user_callback, callback, user_data);
+    return user_callback;
+}
+
+static void
+gjs_debug_user_callback_free(GjsDebugUserCallback *user_callback)
+{
+    g_free(user_callback);
+}
+
+typedef struct _GjsDebugScriptLookupInfo
+{
+    char         *name;
+    unsigned int lineno;
+} GjsDebugScriptLookupInfo;
+
+static GjsDebugScriptLookupInfo *
+gjs_debug_script_lookup_info_new(const char   *name,
+                                 unsigned int  lineno)
+{
+    GjsDebugScriptLookupInfo *info = g_new0(GjsDebugScriptLookupInfo, 1);
+    info->name = g_strdup(name);
+    info->lineno = lineno;
+    return info;
+}
+
+static unsigned int
+gjs_debug_script_lookup_info_hash(gconstpointer key)
+{
+    GjsDebugScriptLookupInfo *info = (GjsDebugScriptLookupInfo *) key;
+    return g_int_hash(&info->lineno) ^ g_str_hash(info->name);
+}
+
+static gboolean
+gjs_debug_script_lookup_info_equal(gconstpointer first,
+                                   gconstpointer second)
+{
+    GjsDebugScriptLookupInfo *first_info = (GjsDebugScriptLookupInfo *) first;
+    GjsDebugScriptLookupInfo *second_info = (GjsDebugScriptLookupInfo *) second;
+
+    return first_info->lineno == second_info->lineno &&
+           g_strcmp0(first_info->name, second_info->name) == 0;
+}
+
+static void
+gjs_debug_script_lookup_info_destroy(gpointer info)
+{
+    GjsDebugScriptLookupInfo *lookup_info = (GjsDebugScriptLookupInfo *) info;
+    g_free(lookup_info->name);
+    g_free(lookup_info);
+}
+
+typedef struct _InterruptCallbackDispatchData {
+    GjsDebugHooks   *hooks;
+    GjsLocationInfo *info;
+} InterruptCallbackDispatchData;
+
+static void
+dispatch_interrupt_callback(gpointer element,
+                            gpointer user_data)
+{
+    GjsDebugUserCallback          *user_callback = (GjsDebugUserCallback *) element;
+    InterruptCallbackDispatchData *dispatch_data = (InterruptCallbackDispatchData *) user_data;
+    GjsDebugHooksPrivate          *priv = (GjsDebugHooksPrivate *) 
gjs_debug_hooks_get_instance_private(dispatch_data->hooks);
+    GjsContext                    *context = priv->context;
+    GjsInterruptCallback           callback = (GjsInterruptCallback) user_callback->callback;
+
+    callback(dispatch_data->hooks,
+             context,
+             dispatch_data->info,
+             user_callback->user_data);
+}
+
+typedef struct _InfoCallbackDispatchData {
+    GjsDebugHooks      *hooks;
+    GjsDebugScriptInfo *info;
+} InfoCallbackDispatchData;
+
+static void
+dispatch_info_callback(gpointer element,
+                       gpointer user_data)
+
+{
+    GjsDebugUserCallback            *user_callback = (GjsDebugUserCallback *) element;
+    InfoCallbackDispatchData        *dispatch_data = (InfoCallbackDispatchData *) user_data;
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) 
gjs_debug_hooks_get_instance_private(dispatch_data->hooks);
+    GjsContext                      *context = priv->context;
+    GjsInfoCallback                 callback = (GjsInfoCallback) user_callback->callback;
+
+    callback(dispatch_data->hooks,
+             context,
+             dispatch_data->info,
+             user_callback->user_data);
+}
+
+typedef struct _FrameCallbackDispatchData {
+  GjsDebugHooks    *hooks;
+  GjsLocationInfo  *info;
+  GjsFrameState    state;
+} FrameCallbackDispatchData;
+
+static void
+dispatch_frame_callbacks(gpointer element,
+                         gpointer user_data)
+
+{
+    GjsDebugUserCallback      *user_callback = (GjsDebugUserCallback *) element;
+    FrameCallbackDispatchData *dispatch_data = (FrameCallbackDispatchData *) user_data;
+    GjsDebugHooksPrivate      *priv = (GjsDebugHooksPrivate *) 
gjs_debug_hooks_get_instance_private(dispatch_data->hooks);
+    GjsContext                *context = priv->context;
+    GjsFrameCallback          callback = (GjsFrameCallback) user_callback->callback;
+
+    callback(dispatch_data->hooks,
+             context,
+             dispatch_data->info,
+             dispatch_data->state,
+             user_callback->user_data);
+}
+
+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 char *
+get_fully_qualified_path(const char *filename)
+{
+    /* If this "filename" is actually a URI then just strip the URI
+     * header and return a copy of the string */
+    char *potential_uri = g_uri_parse_scheme(filename);
+    if (potential_uri) {
+        g_free(potential_uri);
+        return g_strdup(filename);
+    }
+
+    char *fully_qualified_path = NULL;
+    /* Sometimes we might get just a basename if the script is in the current
+     * working directly. If that's the case, then we need to add the fully
+     * qualified pathname */
+    if (!g_path_is_absolute(filename)) {
+        char *current_dir = g_get_current_dir();
+        fully_qualified_path = g_strconcat(current_dir,
+                                           "/",
+                                           filename,
+                                           NULL);
+        g_free(current_dir);
+    } else {
+        fully_qualified_path = g_strdup(filename);
+    }
+
+    return fully_qualified_path;
+}
+
+static void
+populate_frame_info_for_location_info_and_pc (GjsLocationInfo *info,
+                                              GjsFrameInfo    *frame,
+                                              JSContext       *context,
+                                              JSScript        *script,
+                                              JSFunction      *function,
+                                              jsbytecode      *pc,
+                                              jsbytecode      *current_function_pc)
+{
+    frame->current_function.filename = info->filename;
+    frame->current_function.function_name = info->current_function_name;
+    frame->current_function.line = JS_PCToLineNumber(context,
+                                                     script,
+                                                     current_function_pc);
+
+    if (function)
+        frame->current_function.n_args = JS_GetFunctionArgumentCount(context, function);
+    else
+        frame->current_function.n_args = 0;
+
+    frame->current_line = JS_PCToLineNumber(context, script, pc);
+}
+
+static void
+populate_location_info_from_js_function(GjsLocationInfo *info,
+                                        JSContext       *js_context,
+                                        JSScript        *script,
+                                        JSFunction      *js_function)
+{
+    JSString *js_function_name = NULL;
+
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    info->filename = get_fully_qualified_path(JS_GetScriptFilename(js_context, script));
+
+    char *function_name = NULL;
+
+    /* Only set the function name if we're actually in a function */
+    if (js_function)
+    {
+        js_function_name = JS_GetFunctionId(js_function);
+
+        if (!js_function_name) {
+            function_name = g_strdup_printf("(anonymous)");
+        } else {
+            if (!gjs_string_to_utf8(js_context,
+                                    STRING_TO_JSVAL(js_function_name),
+                                    &function_name))
+                gjs_throw(js_context, "Failed to convert function name to utf8 string!");
+        }
+    }
+
+    info->current_function_name = function_name;
+}
+
+static void
+gjs_debug_hooks_populate_location_info(GjsLocationInfo *info,
+                                       JSContext       *js_context,
+                                       JSScript        *script,
+                                       jsbytecode      *current_line_pc,
+                                       jsbytecode      *current_function_pc)
+{
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    JSFunction *js_function  = JS_GetScriptFunction(js_context, script);
+    populate_location_info_from_js_function(info, js_context, script, js_function);
+    populate_frame_info_for_location_info_and_pc(info,
+                                                 &info->current_frame,
+                                                 js_context,
+                                                 script,
+                                                 js_function,
+                                                 current_line_pc,
+                                                 current_function_pc);
+}
+
+static void
+gjs_debug_hooks_clear_location_info(GjsLocationInfo *info)
+{
+    g_free(info->current_function_name);
+    g_free(info->filename);
+}
+
+static void
+gjs_debug_hooks_populate_script_info(GjsDebugScriptInfo *info,
+                                     JSContext          *js_context,
+                                     JSScript           *script,
+                                     const char         *filename)
+{
+    info->filename = filename;
+    info->begin_line = JS_GetScriptBaseLineNumber(js_context, script);
+}
+
+typedef struct _GjsBreakpoint {
+    JSScript *script;
+    jsbytecode *pc;
+} GjsBreakpoint;
+
+static void
+gjs_breakpoint_destroy(gpointer data)
+{
+    GjsBreakpoint *breakpoint = (GjsBreakpoint *) data;
+
+    g_free(breakpoint);
+}
+
+static GjsBreakpoint *
+gjs_breakpoint_new(JSScript   *script,
+                   jsbytecode *pc)
+{
+    GjsBreakpoint *breakpoint = g_new0(GjsBreakpoint, 1);
+    breakpoint->script = script;
+    breakpoint->pc = pc;
+    return breakpoint;
+}
+
+typedef struct _GjsPendingBreakpoint {
+    char         *filename;
+    unsigned int lineno;
+} GjsPendingBreakpoint;
+
+static void
+gjs_pending_breakpoint_destroy(gpointer data)
+{
+    GjsPendingBreakpoint *pending = (GjsPendingBreakpoint *) data;
+    g_free(pending->filename);
+    g_free(pending);
+}
+
+static GjsPendingBreakpoint *
+gjs_pending_breakpoint_new(const char   *filename,
+                           unsigned int  lineno)
+{
+    GjsPendingBreakpoint *pending = g_new0(GjsPendingBreakpoint, 1);
+    pending->filename = g_strdup(filename);
+    pending->lineno = lineno;
+    return pending;
+}
+
+typedef struct _BreakpointActivationData {
+    GjsDebugHooks *debug_hooks;
+    JSContext                *js_context;
+    JSScript                 *js_script;
+    const char               *filename;
+    unsigned int             begin_lineno;
+    GHashTable               *breakpoints;
+    GList                    *breakpoints_changed;
+} BreakpointActivationData;
+
+static void
+remove_breakpoint_from_hashtable_by_user_callback(gpointer list_item,
+                                                  gpointer hashtable_pointer)
+{
+    GHashTable           *breakpoints = (GHashTable *) hashtable_pointer;
+    GjsPendingBreakpoint *pending_breakpoint = (GjsPendingBreakpoint *) g_hash_table_lookup(breakpoints, 
list_item);
+
+    g_return_if_fail(pending_breakpoint);
+
+    gjs_pending_breakpoint_destroy(pending_breakpoint);
+    g_hash_table_remove(breakpoints, list_item);
+}
+
+static void
+remove_activated_breakpoints_from_pending(GList      *activated_breakpoints,
+                                          GHashTable *pending)
+{
+    g_list_foreach(activated_breakpoints,
+                   remove_breakpoint_from_hashtable_by_user_callback,
+                   pending);
+    g_list_free(activated_breakpoints);
+}
+
+static unsigned int
+get_script_end_lineno(JSContext *js_context,
+                      JSScript  *js_script)
+{
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    jsbytecode *pc = JS_EndPC(js_context, js_script);
+    return JS_PCToLineNumber(js_context,
+                             js_script,
+                             pc);
+}
+
+typedef struct _GjsMultiplexedDebugHooksTrapPrivateData {
+    GjsDebugHooks *hooks;
+    GjsDebugUserCallback     *user_callback;
+} GjsMultiplexedDebugHooksTrapPrivateData;
+
+GjsMultiplexedDebugHooksTrapPrivateData *
+gjs_debug_hooks_trap_private_data_new(GjsDebugHooks        *hooks,
+                                      GjsDebugUserCallback *user_callback)
+{
+    GjsMultiplexedDebugHooksTrapPrivateData *data =
+        g_new0(GjsMultiplexedDebugHooksTrapPrivateData, 1);
+
+    data->hooks = hooks;
+    data->user_callback = user_callback;
+
+    return data;
+}
+
+static void
+gjs_debug_hooks_trap_private_data_destroy(GjsMultiplexedDebugHooksTrapPrivateData *data)
+{
+    g_free (data);
+}
+
+static jsbytecode *
+tail_for_pc_stack(GArray *pc_stack)
+{
+    return g_array_index(pc_stack, jsbytecode *, pc_stack->len - 1);
+}
+
+/* Callbacks */
+static JSTrapStatus
+gjs_debug_hooks_trap_handler(JSContext  *context,
+                             JSScript   *script,
+                             jsbytecode *pc,
+                             jsval      *rval,
+                             jsval       closure)
+{
+    GjsMultiplexedDebugHooksTrapPrivateData *data =
+        (GjsMultiplexedDebugHooksTrapPrivateData *) JSVAL_TO_PRIVATE(closure);
+
+    /* And there goes the law of demeter */
+    GjsDebugHooks *hooks = data->hooks;
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    GjsLocationInfo location_info;
+    GjsInterruptCallback callback = (GjsInterruptCallback) data->user_callback->callback;
+    gjs_debug_hooks_populate_location_info(&location_info,
+                                           context,
+                                           script,
+                                           pc,
+                                           tail_for_pc_stack(priv->pc_stack));
+
+    callback(hooks,
+             priv->context,
+             &location_info,
+             data->user_callback->user_data);
+
+    gjs_debug_hooks_clear_location_info(&location_info);
+
+    return JSTRAP_CONTINUE;
+}
+
+static GjsBreakpoint *
+gjs_debug_create_native_breakpoint_for_script(GjsDebugHooks        *hooks,
+                                              JSContext            *js_context,
+                                              JSScript             *script,
+                                              unsigned int          line,
+                                              GjsDebugUserCallback *user_callback)
+{
+    GjsMultiplexedDebugHooksTrapPrivateData *data =
+        gjs_debug_hooks_trap_private_data_new(hooks, user_callback);
+
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    /* This always succeeds, although it might only return the very-end
+     * or very-beginning program counter if the line is out of range */
+    jsbytecode *pc =
+        JS_LineNumberToPC(js_context, script, line);
+
+    /* Set the breakpoint on the JS side now that we're tracking it */
+    JS_SetTrap(js_context,
+               script,
+               pc,
+               gjs_debug_hooks_trap_handler,
+               PRIVATE_TO_JSVAL(data));
+
+    return gjs_breakpoint_new(script, pc);
+}
+
+static GjsBreakpoint *
+create_native_breakpoint_if_within_script(GjsDebugHooks        *debug_hooks,
+                                          JSContext            *context,
+                                          JSScript             *script,
+                                          GjsDebugUserCallback *user_callback,
+                                          GjsPendingBreakpoint *pending,
+                                          const char           *filename,
+                                          unsigned int          begin_lineno)
+
+{
+    /* Interrogate the script for its last program counter and thus its
+     * last line. If the desired breakpoint line falls within this script's
+     * line range then activate it. */
+    if (strcmp(filename, pending->filename) == 0) {
+        unsigned int end_lineno = get_script_end_lineno(context, script);
+
+        if (begin_lineno <= pending->lineno &&
+            end_lineno >= pending->lineno) {
+            GjsBreakpoint *breakpoint =
+                gjs_debug_create_native_breakpoint_for_script(debug_hooks,
+                                                              context,
+                                                              script,
+                                                              pending->lineno,
+                                                              user_callback);
+
+            return breakpoint;
+        }
+    }
+
+    return NULL;
+}
+
+static void
+gjs_debug_hooks_new_script_callback(JSContext    *context,
+                                    const char  *filename,
+                                    unsigned int  lineno,
+                                    JSScript     *script,
+                                    JSFunction   *function,
+                                    gpointer      caller_data)
+{
+    /* We don't care about NULL-filename scripts, they are probably just initialization
+     * scripts */
+    if (!filename)
+        return;
+
+    GjsDebugHooks *hooks = GJS_DEBUG_HOOKS(caller_data);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    GjsDebugScriptLookupInfo *info =
+        gjs_debug_script_lookup_info_new(filename, lineno);
+
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+    char *fully_qualified_path = get_fully_qualified_path(filename);
+
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    g_hash_table_insert(priv->scripts_loaded,
+                        info,
+                        script);
+
+    /* Special case - if single-step mode is enabled then we should enable it
+     * here */
+    if (priv->single_step_mode_usage_count)
+        JS_SetSingleStepMode(js_context, script, TRUE);
+
+    /* Special case - search pending breakpoints for the current script filename
+     * and convert them to real breakpoints if need be */
+    GHashTableIter iter;
+    gpointer       key, value;
+
+    GList *breakpoints_changed = NULL;
+
+    g_hash_table_iter_init(&iter, priv->pending_breakpoints);
+    while (g_hash_table_iter_next(&iter, &key, &value)) {
+        GjsDebugUserCallback *user_callback = (GjsDebugUserCallback *) key;
+        GjsPendingBreakpoint *pending = (GjsPendingBreakpoint *) value;
+        GjsBreakpoint        *breakpoint =
+            create_native_breakpoint_if_within_script(hooks,
+                                                      js_context,
+                                                      script,
+                                                      user_callback,
+                                                      pending,
+                                                      fully_qualified_path,
+                                                      lineno);
+
+        if (breakpoint) {
+            g_hash_table_insert(priv->breakpoints,
+                                user_callback,
+                                breakpoint);
+
+            /* We append user_callback here as that is what we need to remove-by later */
+            breakpoints_changed =
+                g_list_append(breakpoints_changed,
+                              user_callback);
+
+            /* Decrement new script callback, we might not need to know about
+             * new scripts anymore as the breakpoint is no longer pending */
+            gjs_debug_hooks_finish_using_new_script_callback(hooks);
+        }
+    }
+
+    remove_activated_breakpoints_from_pending(breakpoints_changed,
+                                              priv->pending_breakpoints);
+
+    GjsDebugScriptInfo debug_script_info;
+    gjs_debug_hooks_populate_script_info(&debug_script_info,
+                                         context,
+                                         script,
+                                         fully_qualified_path);
+
+
+    InfoCallbackDispatchData data = {
+        hooks,
+        &debug_script_info
+    };
+
+    /* Finally, call the callback function */
+    for_each_element_in_array(priv->new_script_hooks,
+                              dispatch_info_callback,
+                              &data);
+
+    g_free(fully_qualified_path);
+}
+
+static void
+gjs_debug_hooks_script_destroyed_callback(JSFreeOp     *fo,
+                                          JSScript     *script,
+                                          gpointer      caller_data)
+{
+    GjsDebugHooks                   *hooks = GJS_DEBUG_HOOKS(caller_data);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    GjsDebugScriptLookupInfo info = {
+        (char *) JS_GetScriptFilename(js_context, script),
+        JS_GetScriptBaseLineNumber(js_context, script)
+    };
+
+    g_hash_table_remove(priv->scripts_loaded, &info);
+}
+
+static JSTrapStatus
+gjs_debug_hooks_interrupt_callback(JSContext  *context,
+                                   JSScript   *script,
+                                   jsbytecode *pc,
+                                   jsval      *rval,
+                                   gpointer    closure)
+{
+    GjsDebugHooks                   *hooks = GJS_DEBUG_HOOKS(closure);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    GjsLocationInfo location_info;
+    gjs_debug_hooks_populate_location_info(&location_info,
+                                           context,
+                                           script,
+                                           pc,
+                                           tail_for_pc_stack(priv->pc_stack));
+
+    InterruptCallbackDispatchData data = {
+        hooks,
+        &location_info,
+    };
+
+    for_each_element_in_array(priv->single_step_hooks,
+                              dispatch_interrupt_callback,
+                              &data);
+
+    gjs_debug_hooks_clear_location_info(&location_info);
+
+    return JSTRAP_CONTINUE;
+}
+
+static void *
+gjs_debug_hooks_frame_step_callback(JSContext          *context,
+                                    JSAbstractFramePtr  frame,
+                                    bool                is_constructing,
+                                    JSBool              before,
+                                    JSBool             *ok,
+                                    gpointer            closure)
+{
+    JSFunction           *function = frame.maybeFun();
+    JSScript             *script = frame.script();
+    GjsDebugHooks        *hooks = GJS_DEBUG_HOOKS(closure);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    JSBrokenFrameIterator it(context);
+    jsbytecode *current_pc = it.pc();
+    jsbytecode *stack_frame_pc = NULL;
+
+    /* If we are entering a new stack frame, then push the current
+     * program counter on to our array. The tail of the array will
+     * always be the line number of the frame that we're in */
+    if (before) {
+        g_array_append_val(priv->pc_stack, current_pc);
+        stack_frame_pc = current_pc;
+    } else {
+        stack_frame_pc = tail_for_pc_stack (priv->pc_stack);
+        g_array_set_size(priv->pc_stack, priv->pc_stack->len - 1);
+    }
+
+    GjsLocationInfo info;
+    gjs_debug_hooks_populate_location_info(&info,
+                                           context,
+                                           script,
+                                           current_pc,
+                                           current_pc);
+
+    FrameCallbackDispatchData data = {
+        hooks,
+        &info,
+        before ? GJS_FRAME_ENTRY : GJS_FRAME_EXIT
+    };
+
+    for_each_element_in_array(priv->call_and_execute_hooks,
+                              dispatch_frame_callbacks,
+                              &data);
+
+    gjs_debug_hooks_clear_location_info(&info);
+
+    return closure;
+}
+
+static void
+change_debug_mode(GjsDebugHooks *hooks,
+                  unsigned int   flags,
+                  gboolean       enabled)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+    JSAutoCompartment ac(context,
+                         JS_GetGlobalObject(context));
+
+    JS_BeginRequest(context);
+    JS_SetOptions(context, flags);
+    JS_SetDebugMode(context, enabled);
+    JS_EndRequest(context);
+}
+
+static void
+gjs_debug_hooks_use_debug_mode(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (priv->debug_mode_usage_count++ == 0) {
+        change_debug_mode(hooks,
+                          JSOPTION_BASELINE | JSOPTION_TYPE_INFERENCE,
+                          TRUE);
+    }
+}
+
+static void
+gjs_debug_hooks_finish_using_debug_mode(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (--priv->debug_mode_usage_count == 0) {
+        change_debug_mode(hooks,
+                          0,
+                          FALSE);
+    }
+}
+
+static void
+set_interrupt_function_hook(JSContext       *context,
+                            JSInterruptHook  callback,
+                            gpointer         callback_user_data)
+{
+    JSAutoCompartment ac(context,
+                         JS_GetGlobalObject(context));
+
+    JS_SetInterrupt(JS_GetRuntime(context),
+                    callback,
+                    callback_user_data);
+}
+
+static void
+gjs_debug_hooks_use_interrupt_function(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (priv->interrupt_function_usage_count++ == 0) {
+        set_interrupt_function_hook((JSContext *) gjs_context_get_native_context(priv->context),
+                                    gjs_debug_hooks_interrupt_callback,
+                                    hooks);
+    }
+}
+
+static void
+gjs_debug_hooks_finish_using_interrupt_function(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (--priv->interrupt_function_usage_count == 0) {
+        set_interrupt_function_hook((JSContext *) gjs_context_get_native_context(priv->context),
+                                    NULL,
+                                    NULL);
+    }
+}
+
+static void
+set_new_script_hook(JSContext           *context,
+                    JSNewScriptHook      new_callback,
+                    JSDestroyScriptHook  destroy_callback,
+                    gpointer             callback_user_data)
+{
+    JSAutoCompartment ac(context,
+                        JS_GetGlobalObject(context));
+
+    JS_SetNewScriptHook(JS_GetRuntime(context), new_callback, callback_user_data);
+    JS_SetDestroyScriptHook(JS_GetRuntime(context), destroy_callback, callback_user_data);
+}
+
+static void
+gjs_debug_hooks_use_new_script_callback(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (priv->new_script_hook_usage_count++ == 0) {
+        set_new_script_hook((JSContext *) gjs_context_get_native_context(priv->context),
+                            gjs_debug_hooks_new_script_callback,
+                            gjs_debug_hooks_script_destroyed_callback,
+                            hooks);
+    }
+}
+
+static void
+gjs_debug_hooks_finish_using_new_script_callback(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (--priv->new_script_hook_usage_count == 0) {
+        set_new_script_hook((JSContext *) gjs_context_get_native_context(priv->context),
+                            NULL,
+                            NULL,
+                            NULL);
+    }
+}
+
+static void
+set_single_step_mode_on_registered_script(JSScript  *script,
+                                          JSContext *context,
+                                          gboolean   enabled)
+{
+    JSAutoCompartment ac(context,
+                         JS_GetGlobalObject(context));
+
+    JS_SetSingleStepMode(context,
+                         script,
+                         enabled);
+}
+
+static void
+set_single_step_mode(JSContext  *context,
+                     GHashTable *scripts,
+                     gboolean    enabled)
+{
+    GHashTableIter iter;
+    gpointer       key, value;
+
+    g_hash_table_iter_init(&iter, scripts);
+    while (g_hash_table_iter_next(&iter, &key, &value)) {
+        JSScript *script = (JSScript *) value;
+        set_single_step_mode_on_registered_script(script, context, enabled);
+    }
+}
+
+static void
+gjs_debug_hooks_use_single_step_mode(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (priv->single_step_mode_usage_count++ == 0) {
+        set_single_step_mode((JSContext *) gjs_context_get_native_context(priv->context),
+                             priv->scripts_loaded,
+                             TRUE);
+    }
+}
+
+static void
+gjs_debug_hooks_finish_using_single_step_mode(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (--priv->single_step_mode_usage_count == 0) {
+        set_single_step_mode((JSContext *) gjs_context_get_native_context(priv->context),
+                             priv->scripts_loaded,
+                             FALSE);
+    }
+}
+
+static void
+set_frame_execution_hooks(GjsDebugHooks     *hooks,
+                          JSContext         *context,
+                          JSInterpreterHook  hook,
+                          gpointer           hook_user_data)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    JSRuntime *js_runtime = JS_GetRuntime(context);
+
+    JSAutoCompartment ac(context,
+                         JS_GetGlobalObject(context));
+
+    JS_SetExecuteHook(js_runtime, hook, hook_user_data);
+    JS_SetCallHook(js_runtime, hook, hook_user_data);
+
+    /* Make sure to clear the current stack of program
+     * counters either way */
+    g_array_set_size(priv->pc_stack, 0);
+}
+
+static void
+gjs_debug_hooks_use_frame_execution(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (priv->call_and_execute_hook_usage_count++ == 0) {
+        set_frame_execution_hooks(hooks,
+                                  (JSContext *) gjs_context_get_native_context(priv->context),
+                                  gjs_debug_hooks_frame_step_callback,
+                                  hooks);
+    }
+}
+
+static void
+gjs_debug_hooks_finish_using_frame_execution(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    if (--priv->call_and_execute_hook_usage_count == 0) {
+        set_frame_execution_hooks(hooks,
+                                  (JSContext *) gjs_context_get_native_context(priv->context),
+                                  NULL,
+                                  NULL);
+    }
+}
+
+void
+gjs_debug_hooks_remove_breakpoint(GjsDebugHooks *hooks,
+                                  guint          handle)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+    GjsDebugUserCallback *callback =
+        (GjsDebugUserCallback *) g_hash_table_lookup(priv->breakpoints_connections,
+                                                     GINT_TO_POINTER(handle));
+    GjsBreakpoint *breakpoint =
+        (GjsBreakpoint *) g_hash_table_lookup(priv->breakpoints, callback);
+
+    gboolean item_was_removed = FALSE;
+
+    /* Remove breakpoint if there was one */
+    if (breakpoint) {
+        g_hash_table_remove(priv->breakpoints, callback);
+
+        JSAutoCompartment ac(js_context,
+                           JS_GetGlobalObject(js_context));
+
+        jsval previous_closure;
+
+        JS_ClearTrap(js_context,
+                     breakpoint->script,
+                     breakpoint->pc,
+                     NULL,
+                     &previous_closure);
+
+        GjsMultiplexedDebugHooksTrapPrivateData *private_data =
+            (GjsMultiplexedDebugHooksTrapPrivateData *) JSVAL_TO_PRIVATE(previous_closure);
+        gjs_debug_hooks_trap_private_data_destroy(private_data);
+
+        gjs_breakpoint_destroy(breakpoint);
+        item_was_removed = TRUE;
+    } else {
+        /* Try to find pending breakpoints we never got to insert */
+        GjsPendingBreakpoint *pending_breakpoint =
+            (GjsPendingBreakpoint *) g_hash_table_lookup(priv->pending_breakpoints, callback);
+
+        if (pending_breakpoint) {
+            g_hash_table_remove(priv->pending_breakpoints, callback);
+            gjs_pending_breakpoint_destroy(pending_breakpoint);
+
+            /* When removing a pending breakpoint, we must also finish using the new
+             * script hook as we might not care about new scripts anymore if pending
+             * breakpoints are empty */
+            gjs_debug_hooks_finish_using_new_script_callback(hooks);
+
+            item_was_removed = TRUE;
+        }
+    }
+
+    g_assert(item_was_removed);
+
+    g_hash_table_remove(priv->breakpoints_connections, GINT_TO_POINTER(handle));
+    gjs_debug_user_callback_free(callback);
+
+    gjs_debug_hooks_finish_using_frame_execution(hooks);
+    gjs_debug_hooks_finish_using_debug_mode(hooks);
+}
+
+/* Search for a script which has the closest start line to our requested line number */
+static JSScript *
+lookup_script_for_filename_with_closest_start_line(GjsDebugHooks *hooks,
+                                                   const char    *filename,
+                                                   unsigned int   line)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    JSContext      *js_context = (JSContext *) gjs_context_get_native_context(priv->context);
+    GHashTableIter hash_table_iterator;
+    gpointer       key = NULL;
+    gpointer       value = NULL;
+
+    g_hash_table_iter_init(&hash_table_iterator, priv->scripts_loaded);
+
+    while (g_hash_table_iter_next(&hash_table_iterator, &key, &value)) {
+        GjsDebugScriptLookupInfo *info = (GjsDebugScriptLookupInfo *) key;
+
+        if (g_strcmp0(info->name, filename) == 0) {
+            JSScript     *script = (JSScript *) value;
+            unsigned int script_end_line = get_script_end_lineno(js_context,
+                                                                 script);
+
+            if (info->lineno <= line &&
+                script_end_line >= line)
+                return script;
+        }
+    }
+
+    return NULL;
+}
+
+static GjsBreakpoint *
+lookup_line_and_create_native_breakpoint(JSContext            *js_context,
+                                         GjsDebugHooks        *debug_hooks,
+                                         const char           *filename,
+                                         unsigned int          line,
+                                         GjsDebugUserCallback *user_callback)
+{
+    JSScript *script =
+        lookup_script_for_filename_with_closest_start_line(debug_hooks,
+                                                           filename,
+                                                           line);
+
+    if (!script)
+        return NULL;
+
+    return gjs_debug_create_native_breakpoint_for_script(debug_hooks,
+                                                         js_context,
+                                                         script,
+                                                         line,
+                                                         user_callback);
+}
+
+guint
+gjs_debug_hooks_add_breakpoint(GjsDebugHooks        *hooks,
+                               const char           *filename,
+                               unsigned int          line,
+                               GjsInterruptCallback  callback,
+                               gpointer              user_data)
+{
+    static guint debug_hooks_counter = 0;
+
+    GjsDebugHooks        *debug_hooks = GJS_DEBUG_HOOKS(hooks);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(debug_hooks);
+
+    JSContext *js_context =
+        (JSContext *) gjs_context_get_native_context(priv->context);
+
+    /* We always have a user callback even if we couldn't successfully create a native
+     * breakpoint as we can always fall back to creating a pending one */
+    GjsDebugUserCallback *user_callback = gjs_debug_user_callback_new(G_CALLBACK (callback),
+                                                                      user_data);
+    guint connection = ++debug_hooks_counter;
+
+    /* Try to create a native breakpoint. If it succeeds, add it to the breakpoints
+     * table, otherwise create a pending breakpoint */
+    GjsBreakpoint *breakpoint = lookup_line_and_create_native_breakpoint(js_context,
+                                                                         debug_hooks,
+                                                                         filename,
+                                                                         line,
+                                                                         user_callback);
+
+    if (breakpoint) {
+        g_hash_table_insert(priv->breakpoints,
+                            user_callback,
+                            breakpoint);
+    } else {
+        GjsPendingBreakpoint *pending = gjs_pending_breakpoint_new(filename, line);
+        g_hash_table_insert(priv->pending_breakpoints,
+                            user_callback,
+                            pending);
+
+        /* We'll need to know about new scripts being loaded too */
+        gjs_debug_hooks_use_new_script_callback(debug_hooks);
+    }
+
+    g_hash_table_insert(priv->breakpoints_connections,
+                        GINT_TO_POINTER(connection),
+                        user_callback);
+
+    /* We need debug mode for now */
+    gjs_debug_hooks_use_debug_mode(debug_hooks);
+    gjs_debug_hooks_use_frame_execution(debug_hooks);
+
+    return connection;
+}
+
+static int
+lookup_index_by_data_in_array(GArray   *array,
+                              gpointer  data)
+{
+    unsigned int i;
+    gsize element_size = g_array_get_element_size(array);
+    char *underlying_array_pointer = (char *) array->data;
+
+    for (i = 0, underlying_array_pointer = (char *) array->data;
+         i < array->len;
+         ++i, underlying_array_pointer += element_size) {
+        if (data == (gpointer) underlying_array_pointer)
+            return (int) i;
+    }
+
+    return -1;
+}
+
+static guint
+insert_callback_for_hook(GArray     *hooks_array,
+                         GHashTable *hooks_connections_table,
+                         GCallback   callback,
+                         gpointer    user_data)
+{
+    static guint callbacks_hooks_counter = 0;
+
+    unsigned int last_size = hooks_array->len;
+    g_array_set_size(hooks_array,
+                     last_size + 1);
+
+    GjsDebugUserCallback *user_callback =
+        &(g_array_index(hooks_array,
+                        GjsDebugUserCallback,
+                        last_size));
+
+    gjs_debug_user_callback_assign(user_callback,
+                                    callback,
+                                    user_data);
+
+    guint connection = ++callbacks_hooks_counter;
+
+    g_hash_table_insert(hooks_connections_table,
+                        GINT_TO_POINTER(connection),
+                        user_callback);
+
+    return connection;
+}
+
+static void
+remove_callback_for_hook(guint       connection,
+                         GHashTable *hooks_connection_table,
+                         GArray     *hooks_array)
+{
+    GjsDebugUserCallback *user_callback =
+        (GjsDebugUserCallback *) g_hash_table_lookup(hooks_connection_table,
+                                                     GINT_TO_POINTER(connection));
+    int array_index = lookup_index_by_data_in_array(hooks_array,
+                                                    user_callback);
+
+    g_hash_table_remove(hooks_connection_table,
+                        GINT_TO_POINTER(connection));
+
+    if (array_index > -1)
+        g_array_remove_index(hooks_array, array_index);
+    else
+        g_error("Unable to find user callback %p in array index!", user_callback);
+}
+
+void
+gjs_debug_hooks_remove_singlestep_hook(GjsDebugHooks *hooks,
+                                       guint          connection)
+{
+    GjsDebugHooks        *multiplexed_hooks = GJS_DEBUG_HOOKS(hooks);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) 
gjs_debug_hooks_get_instance_private(multiplexed_hooks);
+    remove_callback_for_hook(connection,
+                             priv->single_step_connections,
+                             priv->single_step_hooks);
+
+    gjs_debug_hooks_finish_using_frame_execution(multiplexed_hooks);
+    gjs_debug_hooks_finish_using_interrupt_function(multiplexed_hooks);
+    gjs_debug_hooks_finish_using_single_step_mode(multiplexed_hooks);
+    gjs_debug_hooks_finish_using_new_script_callback(hooks);
+    gjs_debug_hooks_finish_using_debug_mode(multiplexed_hooks);
+}
+
+guint
+gjs_debug_hooks_add_singlestep_hook(GjsDebugHooks        *hooks,
+                                    GjsInterruptCallback  callback,
+                                    gpointer              user_data)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    gjs_debug_hooks_use_debug_mode(hooks);
+    gjs_debug_hooks_use_interrupt_function(hooks);
+    gjs_debug_hooks_use_single_step_mode(hooks);
+    gjs_debug_hooks_use_frame_execution(hooks);
+    gjs_debug_hooks_use_new_script_callback(hooks);
+    return insert_callback_for_hook(priv->single_step_hooks,
+                                    priv->single_step_connections,
+                                    G_CALLBACK(callback),
+                                    user_data);
+}
+
+void
+gjs_debug_hooks_remove_script_load_hook(GjsDebugHooks *hooks,
+                                        guint          connection)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    remove_callback_for_hook(connection,
+                             priv->new_script_connections,
+                             priv->new_script_hooks);
+    gjs_debug_hooks_finish_using_new_script_callback(hooks);
+    gjs_debug_hooks_finish_using_debug_mode(hooks);
+}
+
+guint
+gjs_debug_hooks_add_script_load_hook(GjsDebugHooks   *hooks,
+                                     GjsInfoCallback  callback,
+                                     gpointer         user_data)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    gjs_debug_hooks_use_debug_mode(hooks);
+    gjs_debug_hooks_use_new_script_callback(hooks);
+    return insert_callback_for_hook(priv->new_script_hooks,
+                                    priv->new_script_connections,
+                                    G_CALLBACK(callback),
+                                    user_data);
+}
+
+void
+gjs_debug_hooks_remove_frame_step_hook(GjsDebugHooks *hooks,
+                                       guint          connection)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    remove_callback_for_hook(connection,
+                             priv->call_and_execute_connections,
+                             priv->call_and_execute_hooks);
+    gjs_debug_hooks_finish_using_frame_execution(hooks);
+    gjs_debug_hooks_finish_using_debug_mode(hooks);
+}
+
+guint
+gjs_debug_hooks_add_frame_step_hook(GjsDebugHooks    *hooks,
+                                    GjsFrameCallback  callback,
+                                    gpointer          user_data)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+    gjs_debug_hooks_use_debug_mode(hooks);
+    gjs_debug_hooks_use_frame_execution(hooks);
+    return insert_callback_for_hook(priv->call_and_execute_hooks,
+                                    priv->call_and_execute_connections,
+                                    G_CALLBACK(callback),
+                                    user_data);
+}
+
+static void
+gjs_debug_hooks_init(GjsDebugHooks *hooks)
+{
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    priv->scripts_loaded = g_hash_table_new_full(gjs_debug_script_lookup_info_hash,
+                                                 gjs_debug_script_lookup_info_equal,
+                                                 gjs_debug_script_lookup_info_destroy,
+                                                 NULL);
+
+    priv->breakpoints_connections = g_hash_table_new(g_direct_hash, g_direct_equal);
+    priv->new_script_connections = g_hash_table_new(g_direct_hash, g_direct_equal);
+    priv->call_and_execute_connections = g_hash_table_new(g_direct_hash, g_direct_equal);
+    priv->single_step_connections = g_hash_table_new(g_direct_hash, g_direct_equal);
+
+    priv->breakpoints = g_hash_table_new(g_direct_hash, g_direct_equal);
+    priv->pending_breakpoints = g_hash_table_new(g_direct_hash, g_direct_equal);
+    priv->single_step_hooks = g_array_new(TRUE, TRUE, sizeof(GjsDebugUserCallback));
+    priv->call_and_execute_hooks = g_array_new(TRUE, TRUE, sizeof(GjsDebugUserCallback));
+    priv->new_script_hooks = g_array_new(TRUE, TRUE, sizeof(GjsDebugUserCallback));
+    priv->pc_stack = g_array_new(FALSE, TRUE, sizeof(jsbytecode *));
+}
+
+static void
+unref_all_hashtables(GHashTable **hashtable_array)
+{
+    GHashTable **hashtable_iterator = hashtable_array;
+
+    do {
+        g_assert (g_hash_table_size(*hashtable_iterator) == 0);
+        g_hash_table_unref(*hashtable_iterator);
+    } while (*(++hashtable_iterator));
+}
+
+static void
+destroy_all_arrays(GArray **array_array)
+{
+    GArray **array_iterator = array_array;
+
+    do {
+        g_assert((*array_iterator)->len == 0);
+        g_array_free(*array_iterator, TRUE);
+    } while (*(++array_iterator));
+}
+
+static void
+gjs_debug_hooks_dispose(GObject *object)
+{
+    GjsDebugHooks        *hooks = GJS_DEBUG_HOOKS(object);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    G_OBJECT_CLASS(gjs_debug_hooks_parent_class)->dispose(object);
+}
+
+static void
+gjs_debug_hooks_finalize(GObject *object)
+{
+    GjsDebugHooks        *hooks = GJS_DEBUG_HOOKS(object);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    /* Unref scripts_loaded here as there's no guaruntee it will be empty
+     * since the garbage-collect phase might happen after we're unreffed */
+    g_hash_table_unref(priv->scripts_loaded);
+
+    GHashTable *hashtables_to_unref[] = {
+        priv->breakpoints_connections,
+        priv->new_script_connections,
+        priv->single_step_connections,
+        priv->call_and_execute_connections,
+        priv->breakpoints,
+        priv->pending_breakpoints,
+        NULL
+    };
+
+    GArray *arrays_to_destroy[] = {
+        priv->new_script_hooks,
+        priv->call_and_execute_hooks,
+        priv->single_step_hooks,
+        priv->pc_stack,
+        NULL
+    };
+
+    unref_all_hashtables(hashtables_to_unref);
+    destroy_all_arrays(arrays_to_destroy);
+
+    /* If we've still got usage counts on the context debug hooks then that's
+     * an error and we should assert here */
+    g_assert(priv->call_and_execute_hook_usage_count == 0);
+    g_assert(priv->debug_mode_usage_count == 0);
+    g_assert(priv->interrupt_function_usage_count == 0);
+    g_assert(priv->new_script_hook_usage_count == 0);
+    g_assert(priv->single_step_mode_usage_count == 0);
+    
+    G_OBJECT_CLASS(gjs_debug_hooks_parent_class)->finalize(object);
+}
+
+static void
+gjs_debug_hooks_set_property(GObject      *object,
+                             unsigned int  prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+    GjsDebugHooks                   *hooks = GJS_DEBUG_HOOKS(object);
+    GjsDebugHooksPrivate *priv = (GjsDebugHooksPrivate *) gjs_debug_hooks_get_instance_private(hooks);
+
+    switch (prop_id) {
+    case PROP_CONTEXT:
+        priv->context = GJS_CONTEXT(g_value_get_object(value));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+gjs_debug_hooks_class_init(GjsDebugHooksClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->set_property = gjs_debug_hooks_set_property;
+    object_class->dispose = gjs_debug_hooks_dispose;
+    object_class->finalize = gjs_debug_hooks_finalize;
+
+    properties[PROP_0] = NULL;
+    properties[PROP_CONTEXT] = g_param_spec_object("context",
+                                                   "Context",
+                                                   "GjsContext",
+                                                   GJS_TYPE_CONTEXT,
+                                                   (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+
+    g_object_class_install_properties(object_class,
+                                      PROP_N,
+                                      properties);
+}
+
+GjsDebugHooks *
+gjs_debug_hooks_new(GjsContext *context)
+{
+    GjsDebugHooks *hooks =
+        GJS_DEBUG_HOOKS(g_object_new(GJS_TYPE_DEBUG_HOOKS,
+                                                  "context", context,
+                                                  NULL));
+    return hooks;
+}
diff --git a/gjs/debug-hooks.h b/gjs/debug-hooks.h
new file mode 100644
index 0000000..016b61a
--- /dev/null
+++ b/gjs/debug-hooks.h
@@ -0,0 +1,167 @@
+/*
+ * 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_HOOKS_H
+#define GJS_DEBUG_HOOKS_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GJS_TYPE_DEBUG_HOOKS gjs_debug_hooks_get_type()
+
+#define GJS_DEBUG_HOOKS(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+     GJS_TYPE_DEBUG_HOOKS, GjsDebugHooks))
+
+#define GJS_DEBUG_HOOKS_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), \
+     GJS_TYPE_DEBUG_HOOKS, GjsDebugHooksClass))
+
+#define GJS_IS_DEBUG_HOOKS(obj) \
+    (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+     GJS_TYPE_DEBUG_HOOKS))
+
+#define GJS_IS_DEBUG_HOOKS_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+     GJS_TYPE_DEBUG_HOOKS))
+
+#define GJS_DEBUG_HOOKS_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+     GJS_TYPE_DEBUG_HOOKS, GjsDebugHooksClass))
+
+typedef struct _GjsContext GjsContext;
+typedef struct _GjsReflectedScript GjsReflectedScript;
+typedef struct _GjsDebugScriptInfo GjsDebugScriptInfo;
+typedef struct _GjsLocationInfo GjsLocationInfo;
+typedef struct _GjsFrameInfo GjsFrameInfo;
+
+/* An enum to describe which stage of frame execution we are in.
+ *
+ * An interrupt will be delivered twice per each entered frame, once
+ * upon entry and once upon exit. This information is useful to
+ * some tools, for instance, profilers. BEFORE means before we hit
+ * the function and AFTER means just after its last instruction finished */
+typedef enum _GjsFrameState {
+    GJS_FRAME_ENTRY = 0,
+    GJS_FRAME_EXIT = 1
+} GjsFrameState;
+
+typedef struct _GjsFunctionKey {
+    const char   *filename;
+    const char   *function_name;
+    unsigned int line;
+    unsigned int n_args;
+} GjsFunctionKey;
+
+typedef struct _GjsFrameInfo {
+    unsigned int   current_line;
+    GjsFunctionKey current_function;
+} GjsFrameInfo;
+
+/**
+ * gjs_location_info_get_current_frame:
+ * @info: A #GjsLocationInfo
+ *
+ * This function returns the current stack frame, including function
+ * name and position for a #GjsLocationInfo .
+ *
+ * Return: (transfer-none): A #GjsFrameInfo for the current stack frame
+ */
+const GjsFrameInfo * gjs_location_info_get_current_frame(const GjsLocationInfo *info);
+
+const char * gjs_debug_script_info_get_filename(const GjsDebugScriptInfo *info);
+unsigned int gjs_debug_script_info_get_begin_line(const GjsDebugScriptInfo *info);
+
+typedef struct _GjsDebugHooks GjsDebugHooks;
+typedef struct _GjsDebugHooksClass GjsDebugHooksClass;
+typedef struct _GjsDebugHooksPrivate GjsDebugHooksPrivate;
+
+typedef struct _GjsContext GjsContext;
+
+struct _GjsDebugHooksClass {
+    GObjectClass parent_class;
+};
+
+struct _GjsDebugHooks {
+    GObject parent;
+};
+
+typedef void (*GjsFrameCallback)(GjsDebugHooks   *hooks,
+                                 GjsContext      *context,
+                                 GjsLocationInfo *info,
+                                 GjsFrameState    state,
+                                 gpointer         user_data);
+
+typedef void (*GjsInterruptCallback)(GjsDebugHooks   *hooks,
+                                     GjsContext      *context,
+                                     GjsLocationInfo *info,
+                                     gpointer         user_data);
+
+typedef void (*GjsInfoCallback) (GjsDebugHooks      *hooks,
+                                 GjsContext         *context,
+                                 GjsDebugScriptInfo *info,
+                                 gpointer            user_data);
+
+
+guint
+gjs_debug_hooks_add_breakpoint(GjsDebugHooks        *hooks,
+                               const char           *filename,
+                               unsigned int          line,
+                               GjsInterruptCallback  callback,
+                               gpointer              user_data);
+
+void
+gjs_debug_hooks_remove_breakpoint(GjsDebugHooks *hooks,
+                                  guint          handle);
+
+guint
+gjs_debug_hooks_add_singlestep_hook(GjsDebugHooks        *hooks,
+                                    GjsInterruptCallback  callback,
+                                    gpointer              user_data);
+
+void
+gjs_debug_hooks_remove_singlestep_hook(GjsDebugHooks *hooks,
+                                       guint          handle);
+
+guint
+gjs_debug_hooks_add_script_load_hook(GjsDebugHooks   *hooks,
+                                     GjsInfoCallback  callback,
+                                     gpointer         user_data);
+
+void
+gjs_debug_hooks_remove_script_load_hook(GjsDebugHooks *hooks,
+                                        guint          handle);
+
+guint
+gjs_debug_hooks_add_frame_step_hook(GjsDebugHooks    *hooks,
+                                    GjsFrameCallback  callback,
+                                    gpointer          user_data);
+
+void
+gjs_debug_hooks_remove_frame_step_hook(GjsDebugHooks *hooks,
+                                       guint          handle);
+
+GType gjs_debug_hooks_get_type(void);
+
+GjsDebugHooks * gjs_debug_hooks_new(GjsContext *context);
+
+G_END_DECLS
+
+#endif
diff --git a/test/gjs-test-debug-hooks.cpp b/test/gjs-test-debug-hooks.cpp
new file mode 100644
index 0000000..cf6320e
--- /dev/null
+++ b/test/gjs-test-debug-hooks.cpp
@@ -0,0 +1,875 @@
+/*
+ * 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 <unistd.h>
+#include <sys/types.h>
+
+#include <jsapi.h>
+#include <jsdbgapi.h>
+#include <gjs/gjs.h>
+#include <gjs/jsapi-util.h>
+#include <gjs/debug-hooks.h>
+#include <gjs/reflected-script.h>
+
+typedef struct _GjsDebugHooksFixture {
+    GjsContext     *context;
+    GjsDebugHooks  *debug_hooks;
+    char           *temporary_js_script_filename;
+    int             temporary_js_script_open_handle;
+} GjsDebugHooksFixture;
+
+static void
+gjs_debug_hooks_fixture_set_up (gpointer      fixture_data,
+                                gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char           *js_script = "function f () { return 1; }\n";
+    fixture->context = gjs_context_new();
+    fixture->debug_hooks = gjs_debug_hooks_new(fixture->context);
+    fixture->temporary_js_script_open_handle = g_file_open_tmp("mock-js-XXXXXXX.js",
+                                                               &fixture->temporary_js_script_filename,
+                                                               NULL);
+
+    if (write(fixture->temporary_js_script_open_handle, js_script, strlen(js_script) * sizeof (char)) == -1)
+        g_print("Error writing to temporary file: %s", strerror(errno));
+}
+
+static void
+gjs_debug_hooks_fixture_tear_down(gpointer      fixture_data,
+                                  gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    unlink(fixture->temporary_js_script_filename);
+    g_free(fixture->temporary_js_script_filename);
+    close(fixture->temporary_js_script_open_handle);
+
+    g_object_unref(fixture->debug_hooks);
+    g_object_unref(fixture->context);
+}
+
+typedef guint
+(*GjsDebugHooksConnectionFunction) (GjsDebugHooks *,
+                                    const char    *,
+                                    unsigned int   ,
+                                    GCallback      ,
+                                    gpointer       ,
+                                    GError        *);
+
+static void
+dummy_callback_for_connector(GjsDebugHooks *hooks,
+                             GjsContext    *context,
+                             gpointer       data,
+                             gpointer       user_data)
+{
+}
+
+static guint
+add_dummy_connection_from_function(GjsDebugHooksFixture            *fixture,
+                                   GjsDebugHooksConnectionFunction connector)
+{
+    return (*connector)(fixture->debug_hooks,
+                        fixture->temporary_js_script_filename,
+                        0,
+                        (GCallback) dummy_callback_for_connector,
+                        NULL,
+                        NULL);
+}
+
+typedef void (*GjsDebugHooksDisconnectorFunction) (GjsDebugHooks *hooks, guint);
+
+typedef struct _TestDebugModeStateData {
+    const char                        *componenet_name;
+    GjsDebugHooksConnectionFunction   connector;
+    GjsDebugHooksDisconnectorFunction disconnector;
+} TestDebugModeStateData;
+
+static void
+test_debug_mode_on_while_there_are_active_connections(gpointer      fixture_data,
+                                                      gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    TestDebugModeStateData *data = (TestDebugModeStateData *) user_data;
+    guint connection =
+        add_dummy_connection_from_function(fixture, data->connector);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(fixture->context);
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject(js_context));
+
+    g_assert(JS_GetDebugMode(js_context) == JS_TRUE);
+    data->disconnector(fixture->debug_hooks, connection);
+}
+
+static void
+test_debug_mode_off_when_active_connections_are_released(gpointer      fixture_data,
+                                                         gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    TestDebugModeStateData *data = (TestDebugModeStateData *) user_data;
+    guint connection =
+        add_dummy_connection_from_function(fixture, data->connector);
+    data->disconnector(fixture->debug_hooks, connection);
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(fixture->context);
+    JSAutoCompartment ac(js_context,
+                         JS_GetGlobalObject (js_context));
+
+    g_assert (JS_GetDebugMode (js_context) == JS_FALSE);
+}
+
+static void
+test_fatal_error_when_hook_removed_twice_subprocess(gpointer      fixture_data,
+                                                    gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    TestDebugModeStateData *data = (TestDebugModeStateData *) user_data;
+    guint connection =
+        add_dummy_connection_from_function(fixture, data->connector);
+
+    data->disconnector(fixture->debug_hooks, connection);
+    data->disconnector(fixture->debug_hooks, connection);
+}
+
+static void
+test_fatal_error_when_hook_removed_twice(gpointer      fixture,
+                                         gconstpointer user_data)
+{
+    TestDebugModeStateData *data = (TestDebugModeStateData *) user_data;
+    char *child_test_path =
+        g_build_filename("/gjs/debug_hooks/fatal_error_on_double_remove/subprocess",
+                         data->componenet_name,
+                         NULL);
+    g_test_trap_subprocess(child_test_path,
+                           0,
+                           (GTestSubprocessFlags) (G_TEST_SUBPROCESS_INHERIT_STDERR));
+    g_test_trap_assert_failed();
+
+    g_free(child_test_path);
+}
+
+static void
+single_step_mock_interrupt_callback(GjsDebugHooks   *hooks,
+                                    GjsContext      *context,
+                                    GjsLocationInfo *info,
+                                    gpointer         user_data)
+{
+    unsigned int *hit_count = (unsigned int *) user_data;
+    ++(*hit_count);
+}
+
+static void
+test_interrupts_are_recieved_in_single_step_mode (gpointer      fixture_data,
+                                                  gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    unsigned int hit_count = 0;
+    guint connection =
+        gjs_debug_hooks_add_singlestep_hook(fixture->debug_hooks,
+                                            single_step_mock_interrupt_callback,
+                                            &hit_count);
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+    gjs_debug_hooks_remove_singlestep_hook(fixture->debug_hooks, connection);
+    g_assert(hit_count > 0);
+}
+
+static void
+test_interrupts_are_not_recieved_after_single_step_mode_unlocked (gpointer      fixture_data,
+                                                                  gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    unsigned int hit_count = 0;
+    guint connection =
+        gjs_debug_hooks_add_singlestep_hook(fixture->debug_hooks,
+                                            single_step_mock_interrupt_callback,
+                                            &hit_count);
+    gjs_debug_hooks_remove_singlestep_hook(fixture->debug_hooks, connection);
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+    g_assert(hit_count == 0);
+}
+
+static gboolean
+uint_in_uint_array(unsigned int *array,
+                   unsigned int array_len,
+                   unsigned int n)
+{
+    unsigned int i;
+    for (i = 0; i < array_len; ++i)
+        if (array[i] == n)
+            return TRUE;
+
+    return FALSE;
+}
+
+static void
+single_step_mock_interrupt_callback_tracking_lines (GjsDebugHooks   *hooks,
+                                                    GjsContext      *context,
+                                                    GjsLocationInfo *info,
+                                                    gpointer         user_data)
+{
+    GArray             *line_tracker = (GArray *) user_data;
+    const GjsFrameInfo *frame = gjs_location_info_get_current_frame(info);
+    unsigned int line = frame->current_line;
+
+    if (!uint_in_uint_array((unsigned int *) line_tracker->data,
+                            line_tracker->len,
+                            line))
+        g_array_append_val(line_tracker, line);
+}
+
+static gboolean
+known_executable_lines_are_subset_of_executed_lines(const GArray       *executed_lines,
+                                                    const unsigned int *executable_lines,
+                                                    gsize               executable_lines_len)
+{
+    unsigned int i, j;
+    for (i = 0; i < executable_lines_len; ++i) {
+        gboolean found_executable_line_in_executed_lines = FALSE;
+        for (j = 0; j < executed_lines->len; ++j) {
+            if (g_array_index (executed_lines, unsigned int, j) == executable_lines[i])
+                found_executable_line_in_executed_lines = TRUE;
+        }
+
+        if (!found_executable_line_in_executed_lines)
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+write_content_to_file_at_beginning(int         handle,
+                                   const char *content)
+{
+    if (ftruncate(handle, 0) == -1)
+        g_error ("Failed to erase mock file: %s", strerror(errno));
+    lseek (handle, 0, SEEK_SET);
+    if (write(handle, (gconstpointer) content, strlen(content) * sizeof (char)) == -1)
+        g_error ("Failed to write to mock file: %s", strerror(errno));
+}
+
+static void
+test_interrupts_are_received_on_all_executable_lines_in_single_step_mode (gpointer      fixture_data,
+                                                                          gconstpointer user_data)
+{
+    GArray *line_tracker = g_array_new (FALSE, TRUE, sizeof(unsigned int));
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    guint connection =
+        gjs_debug_hooks_add_singlestep_hook(fixture->debug_hooks,
+                                            single_step_mock_interrupt_callback_tracking_lines,
+                                            line_tracker);
+    const char mock_script[] =
+        "let a = 1;\n" \
+        "let b = 2;\n" \
+        "\n" \
+        "function func (a, b) {\n" \
+        "    let result = a + b;\n" \
+        "    return result;\n" \
+        "}\n" \
+        "\n" \
+        "let c = func (a, b);\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    GjsContext         *reflection_context = gjs_reflected_script_create_reflection_context();
+    GjsReflectedScript *reflected = gjs_reflected_script_new(fixture->temporary_js_script_filename,
+                                                             reflection_context);
+    unsigned int       n_executable_lines = 0;
+    const unsigned int *executable_lines =
+        gjs_reflected_script_get_expression_lines(reflected, &n_executable_lines);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(known_executable_lines_are_subset_of_executed_lines(line_tracker,
+                                                                 executable_lines,
+                                                                 n_executable_lines) == TRUE);
+
+    g_array_free(line_tracker, TRUE);
+    gjs_debug_hooks_remove_singlestep_hook(fixture->debug_hooks, connection);
+    g_object_unref(reflected);
+    g_object_unref(reflection_context);
+}
+
+static void
+mock_breakpoint_callback(GjsDebugHooks   *hooks,
+                         GjsContext      *context,
+                         GjsLocationInfo *info,
+                         gpointer         user_data)
+{
+    unsigned int *line_hit = (unsigned int *) user_data;
+    *line_hit = gjs_location_info_get_current_frame(info)->current_line;
+}
+
+static void
+test_breakpoint_is_hit_when_adding_before_script_run(gpointer      fixture_data,
+                                                     gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "let expected_breakpoint_line = 1;\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    unsigned int line_hit = 0;
+    guint connection =
+        gjs_debug_hooks_add_breakpoint(fixture->debug_hooks,
+                                       fixture->temporary_js_script_filename,
+                                       1,
+                                       mock_breakpoint_callback,
+                                       &line_hit);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(line_hit == 1);
+
+    gjs_debug_hooks_remove_breakpoint(fixture->debug_hooks, connection);
+}
+
+typedef struct _MockNewScriptHookAddBreakpointData {
+    unsigned int connection;
+    unsigned int line;
+    unsigned int hit_count;
+} MockNewScriptHookAddBreakpointData;
+
+static void
+mock_new_script_hook_add_breakpoint(GjsDebugHooks      *hooks,
+                                    GjsContext         *context,
+                                    GjsDebugScriptInfo *info,
+                                    gpointer            user_data)
+{
+    MockNewScriptHookAddBreakpointData *data = (MockNewScriptHookAddBreakpointData *) user_data;
+    data->connection = gjs_debug_hooks_add_breakpoint(hooks,
+                                                      gjs_debug_script_info_get_filename(info),
+                                                      data->line,
+                                                      mock_breakpoint_callback,
+                                                      &data->hit_count);
+}
+
+static void
+test_breakpoint_is_hit_when_adding_during_script_run(gpointer      fixture_data,
+                                                     gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "let expected_breakpoint_line = 1;\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    MockNewScriptHookAddBreakpointData new_script_hook_data = {
+        0,
+        2,
+        0
+    };
+
+    guint new_script_hook_connection =
+        gjs_debug_hooks_add_script_load_hook(fixture->debug_hooks,
+                                             mock_new_script_hook_add_breakpoint,
+                                             &new_script_hook_data);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(new_script_hook_data.hit_count > 1);
+
+    gjs_debug_hooks_remove_breakpoint(fixture->debug_hooks, new_script_hook_data.connection);
+    gjs_debug_hooks_remove_script_load_hook(fixture->debug_hooks, new_script_hook_connection);
+}
+
+static void
+test_breakpoint_is_not_hit_when_later_removed (gpointer      fixture_data,
+                                               gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "let expected_breakpoint_line = 1;\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    unsigned int line_hit = 0;
+    guint connection =
+        gjs_debug_hooks_add_breakpoint(fixture->debug_hooks,
+                                       fixture->temporary_js_script_filename,
+                                       1,
+                                       mock_breakpoint_callback,
+                                       &line_hit);
+    gjs_debug_hooks_remove_breakpoint(fixture->debug_hooks, connection);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(line_hit == 0);
+}
+
+static void
+mock_frame_execution_interrupt_handler(GjsDebugHooks   *hooks,
+                                       GjsContext      *context,
+                                       GjsLocationInfo *info,
+                                       GjsFrameState   state,
+                                       gpointer        user_data)
+{
+    gboolean *interrupts_received = (gboolean *) user_data;
+    *interrupts_received = TRUE;
+}
+
+static void
+test_interrupts_received_when_connected_to_frame_step(gpointer      fixture_data,
+                                                      gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    gboolean interrupts_received = FALSE;
+
+    guint connection =
+        gjs_debug_hooks_add_frame_step_hook(fixture->debug_hooks,
+                                            mock_frame_execution_interrupt_handler,
+                                            &interrupts_received);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(interrupts_received == TRUE);
+
+    gjs_debug_hooks_remove_frame_step_hook(fixture->debug_hooks, connection);
+}
+
+static void
+mock_frame_execution_interrupt_handler_recording_functions (GjsDebugHooks   *hooks,
+                                                            GjsContext      *context,
+                                                            GjsLocationInfo *info,
+                                                            GjsFrameState    state,
+                                                            gpointer         user_data)
+{
+    const GjsFrameInfo *frame = gjs_location_info_get_current_frame(info);
+    GList **function_names_hit = (GList **) user_data;
+
+    *function_names_hit = g_list_append (*function_names_hit,
+                                         g_strdup(frame->current_function.function_name));
+}
+
+static gboolean
+check_if_string_elements_are_in_list (GList       *list,
+                                      const char  **elements,
+                                      gsize        n_elements)
+{
+    if (elements && !list)
+        return FALSE;
+
+    unsigned int i;
+    for (i = 0; i < n_elements; ++i) {
+        GList *iter = list;
+        gboolean found = FALSE;
+
+        while (iter) {
+            if (g_strcmp0 ((const char *) iter->data, elements[i]) == 0) {
+                found = TRUE;
+                break;
+            }
+
+            iter = g_list_next  (iter);
+        }
+
+        if (!found)
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+test_expected_function_names_hit_on_frame_step (gpointer      fixture_data,
+                                                gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "function foo (a) {\n"
+        "    return a;\n"
+        "}\n"
+        "let b = foo (a);\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    GList *function_names_hit = NULL;
+    guint connection =
+        gjs_debug_hooks_add_frame_step_hook(fixture->debug_hooks,
+                                            mock_frame_execution_interrupt_handler_recording_functions,
+                                            &function_names_hit);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    const char *expected_function_names_hit[] = {
+      "foo"
+    };
+    const gsize expected_function_names_hit_len =
+        G_N_ELEMENTS(expected_function_names_hit);
+
+    g_assert(check_if_string_elements_are_in_list(function_names_hit,
+                                                  expected_function_names_hit,
+                                                  expected_function_names_hit_len));
+
+    if (function_names_hit)
+        g_list_free_full(function_names_hit, g_free);
+
+    gjs_debug_hooks_remove_frame_step_hook(fixture->debug_hooks,
+                                           connection);
+}
+
+static void
+test_nothing_hit_when_frame_step_hook_removed (gpointer      fixture_data,
+                                               gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char mock_script[] =
+        "let a = 1;\n"
+        "function foo (a) {\n"
+        "    return a;\n"
+        "}\n"
+        "let b = foo (a);\n"
+        "\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       mock_script);
+
+    GList *function_names_hit = NULL;
+    guint connection =
+        gjs_debug_hooks_add_frame_step_hook(fixture->debug_hooks,
+                                            mock_frame_execution_interrupt_handler_recording_functions,
+                                            &function_names_hit);
+    gjs_debug_hooks_remove_frame_step_hook(fixture->debug_hooks, connection);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(function_names_hit == NULL);
+}
+
+static void
+replace_string(char       **string_pointer,
+               const char *new_string)
+{
+    if (*string_pointer)
+        g_free (*string_pointer);
+
+    *string_pointer = g_strdup (new_string);
+}
+
+static void
+mock_new_script_hook(GjsDebugHooks      *hooks,
+                     GjsContext         *context,
+                     GjsDebugScriptInfo *info,
+                     gpointer            user_data)
+{
+    char **last_loaded_script = (char **) user_data;
+
+    replace_string(last_loaded_script,
+                   gjs_debug_script_info_get_filename(info));
+}
+
+static void
+test_script_load_notification_sent_on_new_script(gpointer      fixture_data,
+                                                 gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char loadable_script[] = "let a = 1;\n\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       loadable_script);
+
+    char *last_loaded_script = NULL;
+    guint connection =
+        gjs_debug_hooks_add_script_load_hook(fixture->debug_hooks,
+                                             mock_new_script_hook,
+                                             &last_loaded_script);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(last_loaded_script != NULL &&
+             g_strcmp0(last_loaded_script,
+                       fixture->temporary_js_script_filename) == 0);
+
+    g_free(last_loaded_script);
+    gjs_debug_hooks_remove_script_load_hook(fixture->debug_hooks, connection);
+}
+
+static void
+test_script_load_notification_not_sent_on_connection_removed(gpointer      fixture_data,
+                                                             gconstpointer user_data)
+{
+    GjsDebugHooksFixture *fixture = (GjsDebugHooksFixture *) fixture_data;
+    const char loadable_script[] = "let a = 1;\n\n";
+
+    write_content_to_file_at_beginning(fixture->temporary_js_script_open_handle,
+                                       loadable_script);
+
+    char *last_loaded_script = NULL;
+    guint connection =
+        gjs_debug_hooks_add_script_load_hook(fixture->debug_hooks,
+                                             mock_new_script_hook,
+                                             &last_loaded_script);
+
+    gjs_debug_hooks_remove_script_load_hook(fixture->debug_hooks, connection);
+
+    gjs_context_eval_file(fixture->context,
+                          fixture->temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    g_assert(last_loaded_script == NULL);
+}
+
+typedef void (*TestDataFunc)(gpointer      data,
+                             gconstpointer user_data);
+
+static void
+for_each_in_table_driven_test_data(gconstpointer test_data,
+                                   gsize         element_size,
+                                   gsize         n_elements,
+                                   TestDataFunc  func,
+                                   gconstpointer user_data)
+{
+    const char *test_data_iterator = (const char *) test_data;
+    gsize i;
+    for (i = 0; i < n_elements; ++i, test_data_iterator += element_size)
+        (*func)((char *) (test_data_iterator), user_data);
+}
+
+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);
+}
+
+typedef struct _FixturedTableDrivenTestData {
+    const char       *test_name;
+    gsize            fixture_size;
+    GTestFixtureFunc set_up;
+    GTestFixtureFunc test_func;
+    GTestFixtureFunc tear_down;
+} FixturedTableDrivenTestData;
+
+static void
+add_test_for_fixture_size_and_funcs(gpointer      data,
+                                    gconstpointer user_data)
+{
+    const FixturedTableDrivenTestData *fixtured_table_driven_test = (const FixturedTableDrivenTestData *) 
data;
+    FixturedTest fixtured_test = {
+        fixtured_table_driven_test->fixture_size,
+        fixtured_table_driven_test->set_up,
+        fixtured_table_driven_test->tear_down
+    };
+    add_test_for_fixture(fixtured_table_driven_test->test_name,
+                         &fixtured_test,
+                         fixtured_table_driven_test->test_func,
+                         user_data);
+
+}
+
+typedef struct _GjsDebugHooksTableDrivenTest {
+    const char      *prefix;
+    GTestFixtureFunc test_function;
+} GjsDebugHooksTableDrivenTest;
+
+static void
+add_gjs_debug_hooks_context_state_data_test(gpointer      data,
+                                                        gconstpointer user_data)
+{
+    const GjsDebugHooksTableDrivenTest *test = (const GjsDebugHooksTableDrivenTest *) user_data;
+    TestDebugModeStateData             *table_data = (TestDebugModeStateData *) data;
+
+    char *test_name = g_strconcat(test->prefix, "/", table_data->componenet_name, NULL);
+
+    FixturedTableDrivenTestData fixtured_data = {
+        test_name,
+        sizeof(GjsDebugHooksFixture),
+        gjs_debug_hooks_fixture_set_up,
+        test->test_function,
+        gjs_debug_hooks_fixture_tear_down
+    };
+
+    add_test_for_fixture_size_and_funcs(&fixtured_data,
+                                        (gconstpointer) table_data);
+
+    g_free (test_name);
+}
+
+void gjs_test_add_tests_for_debug_hooks()
+{
+    static const TestDebugModeStateData context_state_data[] = {
+        {
+            "add_breakpoint",
+            (GjsDebugHooksConnectionFunction) gjs_debug_hooks_add_breakpoint,
+            gjs_debug_hooks_remove_breakpoint
+        },
+        {
+            "singlestep",
+            (GjsDebugHooksConnectionFunction) gjs_debug_hooks_add_singlestep_hook,
+            gjs_debug_hooks_remove_singlestep_hook
+        },
+        {
+            "script_load",
+            (GjsDebugHooksConnectionFunction) gjs_debug_hooks_add_script_load_hook,
+            gjs_debug_hooks_remove_script_load_hook
+        },
+        {
+            "hook_frame_step",
+            (GjsDebugHooksConnectionFunction) gjs_debug_hooks_add_frame_step_hook,
+            gjs_debug_hooks_remove_frame_step_hook
+        }
+    };
+    const gsize context_state_data_len =
+        G_N_ELEMENTS(context_state_data);
+
+    const GjsDebugHooksTableDrivenTest debug_hooks_tests_info[] = {
+        {
+            "/gjs/debug_hooks/debug_mode_on_for",
+            test_debug_mode_on_while_there_are_active_connections
+        },
+        {
+            "/gjs/debug_hooks/debug_mode_off_when_released",
+            test_debug_mode_off_when_active_connections_are_released
+        },
+        {
+            "/gjs/debug_hooks/fatal_error_on_double_remove",
+            test_fatal_error_when_hook_removed_twice
+        },
+        {
+            "/gjs/debug_hooks/fatal_error_on_double_remove/subprocess",
+            test_fatal_error_when_hook_removed_twice_subprocess
+        }
+    };
+    const gsize debug_hooks_tests_info_size =
+        G_N_ELEMENTS(debug_hooks_tests_info);
+
+    gsize i;
+    for (i = 0; i < debug_hooks_tests_info_size; ++i)
+        for_each_in_table_driven_test_data(&context_state_data,
+                                           sizeof(TestDebugModeStateData),
+                                           context_state_data_len,
+                                           add_gjs_debug_hooks_context_state_data_test,
+                                           (gconstpointer) &debug_hooks_tests_info[i]);
+
+    FixturedTest debug_hooks_fixture = {
+        sizeof (GjsDebugHooksFixture),
+        gjs_debug_hooks_fixture_set_up,
+        gjs_debug_hooks_fixture_tear_down
+    };
+
+    add_test_for_fixture("/gjs/debug_hooks/interrupts_recieved_when_in_single_step_mode",
+                         &debug_hooks_fixture,
+                         test_interrupts_are_recieved_in_single_step_mode,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/no_interrupts_after_singlestep_removed",
+                         &debug_hooks_fixture,
+                         test_interrupts_are_not_recieved_after_single_step_mode_unlocked,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/interrupts_received_on_expected_lines_of_script",
+                         &debug_hooks_fixture,
+                         test_interrupts_are_received_on_all_executable_lines_in_single_step_mode,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/breakpoint_hit_when_added_before_script_run",
+                         &debug_hooks_fixture,
+                         test_breakpoint_is_hit_when_adding_before_script_run,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/breakpoint_hit_when_added_during_script_run",
+                         &debug_hooks_fixture,
+                         test_breakpoint_is_hit_when_adding_during_script_run,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/breakpoint_not_hit_after_removal",
+                         &debug_hooks_fixture,
+                         test_breakpoint_is_not_hit_when_later_removed,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/interrupts_on_frame_execution",
+                         &debug_hooks_fixture,
+                         test_interrupts_received_when_connected_to_frame_step,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/interrupts_for_expectected_on_frame_execution",
+                         &debug_hooks_fixture,
+                         test_expected_function_names_hit_on_frame_step,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/no_interrupts_on_frame_execution_removed",
+                         &debug_hooks_fixture,
+                         test_nothing_hit_when_frame_step_hook_removed,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/new_script_notification",
+                         &debug_hooks_fixture,
+                         test_script_load_notification_sent_on_new_script,
+                         NULL);
+    add_test_for_fixture("/gjs/debug_hooks/no_script_notification_on_hook_removed",
+                         &debug_hooks_fixture,
+                         test_script_load_notification_not_sent_on_connection_removed,
+                         NULL);
+}
diff --git a/test/gjs-tests-add-funcs.h b/test/gjs-tests-add-funcs.h
index e872f1c..5080a68 100644
--- a/test/gjs-tests-add-funcs.h
+++ b/test/gjs-tests-add-funcs.h
@@ -21,5 +21,6 @@
 #define GJS_TESTS_ADD_FUNCS_H
 
 void gjs_test_add_tests_for_reflected_script ();
+void gjs_test_add_tests_for_debug_hooks ();
 
 #endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index faf75f7..81219d8 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -399,6 +399,7 @@ main(int    argc,
     g_test_add_func("/util/glib/strv/concat/pointers", gjstest_test_func_util_glib_strv_concat_pointers);
 
     gjs_test_add_tests_for_reflected_script();
+    gjs_test_add_tests_for_debug_hooks ();
 
     g_test_run();
 


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