summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard W.M. Jones <rjones@redhat.com>2012-11-19 13:00:52 +0000
committerRichard W.M. Jones <rjones@redhat.com>2012-11-19 14:01:40 +0000
commitf77ddb9e114e560724d6548499047ae6894ab59c (patch)
tree6341c27ca32fb5e1262f1263974991a1d6357b22
parentd14557d434de5cb1c9754ae0f3e8e820e0a46694 (diff)
downloadlibguestfs-f77ddb9e114e560724d6548499047ae6894ab59c.tar.gz
libguestfs-f77ddb9e114e560724d6548499047ae6894ab59c.tar.xz
libguestfs-f77ddb9e114e560724d6548499047ae6894ab59c.zip
lua: Various fixes and enhancements:
- add support for events (with test) - test progress messages - update documentation to describe events - refactor handle closing code - refactor error code - use 'assert' in test code instead of 'if ... then error end'
-rw-r--r--generator/lua.ml326
-rw-r--r--lua/Makefile.am10
-rw-r--r--lua/examples/guestfs-lua.pod27
-rwxr-xr-xlua/tests/027-create-multiple.lua15
-rwxr-xr-xlua/tests/030-config.lua4
-rwxr-xr-xlua/tests/050-lvcreate.lua7
-rwxr-xr-xlua/tests/060-readdir.lua8
-rwxr-xr-xlua/tests/400-events.lua49
-rwxr-xr-xlua/tests/400-progress.lua44
9 files changed, 435 insertions, 55 deletions
diff --git a/generator/lua.ml b/generator/lua.ml
index 874681c3..fbb4e93a 100644
--- a/generator/lua.ml
+++ b/generator/lua.ml
@@ -53,19 +53,41 @@ let generate_lua_c () =
/* This struct is managed on the Lua heap. If the GC collects it,
* the Lua '__gc' function is called which ends up calling
- * lua_guestfs_finalizer. If we need to store other per-handle
- * data in future, that can be placed into this struct.
+ * lua_guestfs_finalizer.
+ *
+ * There is also an entry in the Lua registry, indexed by 'g'
+ * (allocated on demand) which stores per-handle Lua data. See
+ * functions 'get_per_handle_table', 'free_per_handle_table'.
*/
struct userdata {
guestfs_h *g; /* Libguestfs handle, NULL if closed. */
+ struct event_state *es;
+};
+
+/* Structure passed to event_callback_wrapper. */
+struct event_state {
+ struct event_state *next; /* Stored in a linked list. */
+ lua_State *L;
+ struct userdata *u;
+ int ref; /* Reference to closure. */
};
static struct userdata *get_handle (lua_State *L, int index);
+
+static void get_per_handle_table (lua_State *L, guestfs_h *g);
+static void free_per_handle_table (lua_State *L, guestfs_h *g);
+
static char **get_string_list (lua_State *L, int index);
static void push_string_list (lua_State *L, char **strs);
static void push_table (lua_State *L, char **table);
static int64_t get_int64 (lua_State *L, int index);
static void push_int64 (lua_State *L, int64_t i64);
+static void push_int64_array (lua_State *L, const int64_t *array, size_t len);
+
+static void event_callback_wrapper (guestfs_h *g, void *esvp, uint64_t event, int eh, int flags, const char *buf, size_t buf_len, const uint64_t *array, size_t array_len);
+static uint64_t get_event (lua_State *L, int index);
+static uint64_t get_event_bitmask (lua_State *L, int index);
+static void push_event (lua_State *L, uint64_t event);
";
@@ -80,6 +102,10 @@ static void push_int64 (lua_State *L, int64_t i64);
pr "\
+/* On the stack at 'index' should be a table. Check if 'name' (string)
+ * is a key in this table, and if so execute 'code'. While 'code' is
+ * executing, the top of stack (ie. index == -1) is the value of 'name'.
+ */
#define OPTARG_IF_SET(index, name, code) \\
do { \\
lua_pushliteral (L, name); \\
@@ -123,18 +149,32 @@ lua_guestfs_create (lua_State *L)
lua_setmetatable (L, -2);
u->g = g;
+ u->es = NULL;
return 1;
}
+static void
+close_handle (lua_State *L, guestfs_h *g)
+{
+ guestfs_close (g);
+ free_per_handle_table (L, g);
+}
+
/* Finalizer. */
static int
lua_guestfs_finalizer (lua_State *L)
{
struct userdata *u = get_handle (L, 1);
+ struct event_state *es, *es_next;
if (u->g)
- guestfs_close (u->g);
+ close_handle (L, u->g);
+
+ for (es = u->es; es != NULL; es = es_next) {
+ es_next = es->next;
+ free (es);
+ }
/* u will be freed by Lua when we return. */
@@ -148,13 +188,67 @@ lua_guestfs_close (lua_State *L)
struct userdata *u = get_handle (L, 1);
if (u->g) {
- guestfs_close (u->g);
+ close_handle (L, u->g);
u->g = NULL;
}
return 0;
}
+/* Return the last error in the handle. */
+static int
+last_error (lua_State *L, guestfs_h *g)
+{
+ /* Construct an error object on the stack containing 'msg'
+ * and 'code' fields.
+ */
+ lua_newtable (L);
+ lua_pushliteral (L, \"msg\");
+ lua_pushstring (L, guestfs_last_error (g));
+ lua_settable (L, -3);
+ lua_pushliteral (L, \"code\");
+ lua_pushinteger (L, guestfs_last_errno (g));
+ lua_settable (L, -3);
+
+ /* Raise an exception with the error object. */
+ return lua_error (L);
+}
+
+/* Push the per-handle Lua table onto the stack. This is stored
+ * in the global Lua registry. It is allocated on demand the first
+ * time you call this function. Use luaL_ref to allocate new
+ * entries in this table.
+ * See also: http://www.lua.org/pil/27.3.1.html
+ */
+static void
+get_per_handle_table (lua_State *L, guestfs_h *g)
+{
+ again:
+ lua_pushlightuserdata (L, g);
+ lua_gettable (L, LUA_REGISTRYINDEX);
+ if (lua_isnil (L, -1)) {
+ lua_pop (L, 1);
+ /* registry[g] = {} */
+ lua_pushlightuserdata (L, g);
+ lua_newtable (L);
+ lua_settable (L, LUA_REGISTRYINDEX);
+ goto again;
+ }
+}
+
+/* Free the per-handle Lua table. It doesn't literally \"free\"
+ * anything since the GC will do that. It just removes the entry
+ * from the global registry.
+ */
+static void
+free_per_handle_table (lua_State *L, guestfs_h *g)
+{
+ /* registry[g] = nil */
+ lua_pushlightuserdata (L, g);
+ lua_pushnil (L);
+ lua_settable (L, LUA_REGISTRYINDEX);
+}
+
/* User cancel. */
static int
lua_guestfs_user_cancel (lua_State *L)
@@ -167,8 +261,129 @@ lua_guestfs_user_cancel (lua_State *L)
return 0;
}
+/* Set an event callback. */
+static int
+lua_guestfs_set_event_callback (lua_State *L)
+{
+ struct userdata *u = get_handle (L, 1);
+ guestfs_h *g = u->g;
+ uint64_t event_bitmask;
+ int eh;
+ int ref;
+ struct event_state *es;
+
+ if (g == NULL)
+ return luaL_error (L, \"Guestfs.%%s: handle is closed\",
+ \"set_event_callback\");
+
+ event_bitmask = get_event_bitmask (L, 3);
+
+ /* Save the function in the per-handle table, so that the GC doesn't
+ * clean it up before the event fires.
+ */
+ luaL_checktype (L, 2, LUA_TFUNCTION);
+ get_per_handle_table (L, g);
+ lua_pushvalue (L, 2);
+ ref = luaL_ref (L, -2);
+ lua_pop (L, 1);
+
+ es = malloc (sizeof *es);
+ if (!es)
+ return luaL_error (L, \"failed to allocate event_state\");
+ es->next = u->es;
+ es->L = L;
+ es->u = u;
+ es->ref = ref;
+ u->es = es;
+
+ eh = guestfs_set_event_callback (g, event_callback_wrapper,
+ event_bitmask, 0, es);
+ if (eh == -1)
+ return last_error (L, g);
+
+ /* Return the event handle. */
+ lua_pushinteger (L, eh);
+ return 1;
+}
+
+static void
+event_callback_wrapper (guestfs_h *g,
+ void *esvp,
+ uint64_t event,
+ int eh,
+ int flags,
+ const char *buf, size_t buf_len,
+ const uint64_t *array, size_t array_len)
+{
+ struct event_state *es = esvp;
+ lua_State *L = es->L;
+ struct userdata *u = es->u;
+
+ /* Look up the closure to call in the per-handle table. */
+ get_per_handle_table (L, g);
+ lua_rawgeti (L, -1, es->ref);
+
+ if (!lua_isfunction (L, -1)) {
+ fprintf (stderr, \"lua-guestfs: %%s: internal error: no closure found for g = %%p, eh = %%d\\n\",
+ __func__, g, eh);
+ goto out;
+ }
+
+ /* Call the event handler: event_handler (g, event, eh, flags, buf, array) */
+ lua_pushlightuserdata (L, u); /* XXX correct? */
+ push_event (L, event);
+ lua_pushinteger (L, eh);
+ lua_pushinteger (L, flags);
+ lua_pushlstring (L, buf, buf_len);
+ push_int64_array (L, (const int64_t *) array, array_len);
+
+ switch (lua_pcall (L, 6, 0, 0)) {
+ case 0: /* call ok - do nothing */
+ break;
+ case LUA_ERRRUN:
+ fprintf (stderr, \"lua-guestfs: %%s: unexpected error in event handler\\n\",
+ __func__);
+ /* XXX could print the error instead of throwing it away */
+ lua_pop (L, 1);
+ break;
+ case LUA_ERRERR: /* can probably never happen */
+ fprintf (stderr, \"lua-guestfs: %%s: error calling error handler\\n\",
+ __func__);
+ break;
+ case LUA_ERRMEM:
+ fprintf (stderr, \"lua-guestfs: %%s: memory allocation failed\\n\", __func__);
+ break;
+ default:
+ fprintf (stderr, \"lua-guestfs: %%s: unknown error\\n\", __func__);
+ }
+
+ /* Pop the per-handle table. */
+ out:
+ lua_pop (L, 1);
+}
+
+/* Delete an event callback. */
+static int
+lua_guestfs_delete_event_callback (lua_State *L)
+{
+ struct userdata *u = get_handle (L, 1);
+ guestfs_h *g = u->g;
+ int eh;
+
+ if (g == NULL)
+ return luaL_error (L, \"Guestfs.%%s: handle is closed\",
+ \"delete_event_callback\");
+
+ eh = luaL_checkint (L, 2);
+
+ guestfs_delete_event_callback (g, eh);
+
+ return 0;
+}
+
";
+ (* Actions. *)
List.iter (
fun { name = name; style = (ret, args, optargs as style);
c_function = c_function; c_optarg_prefix = c_optarg_prefix } ->
@@ -249,7 +464,7 @@ lua_guestfs_user_cancel (lua_State *L)
| Bool n ->
pr " %s = lua_toboolean (L, %d);\n" n i
| Int n ->
- pr " %s = lua_tointeger (L, %d);\n" n i
+ pr " %s = luaL_checkint (L, %d);\n" n i
| Int64 n ->
pr " %s = get_int64 (L, %d);\n" n i
| Pointer (t, n) -> assert false
@@ -274,7 +489,7 @@ lua_guestfs_user_cancel (lua_State *L)
| OBool n ->
pr " optargs_s.%s = lua_toboolean (L, -1);\n" n
| OInt n ->
- pr " optargs_s.%s = lua_tointeger (L, -1);\n" n
+ pr " optargs_s.%s = luaL_checkint (L, -1);\n" n
| OInt64 n ->
pr " optargs_s.%s = get_int64 (L, -1);\n" n
| OString n ->
@@ -313,28 +528,15 @@ lua_guestfs_user_cancel (lua_State *L)
) optargs;
(* Handle errors. *)
- let raise_error () =
- pr " lua_newtable (L);\n";
- pr " lua_pushliteral (L, \"msg\");\n";
- pr " lua_pushstring (L, guestfs_last_error (g));\n";
- pr " lua_settable (L, -3);\n";
- pr " lua_pushliteral (L, \"code\");\n";
- pr " lua_pushinteger (L, guestfs_last_errno (g));\n";
- pr " lua_settable (L, -3);\n";
- pr " return lua_error (L);\n"
- in
-
(match errcode_of_ret ret with
| `CannotReturnError -> ()
| `ErrorIsMinusOne ->
- pr " if (r == -1) {\n";
- raise_error ();
- pr " }\n";
+ pr " if (r == -1)\n";
+ pr " return last_error (L, g);\n";
pr "\n"
| `ErrorIsNULL ->
- pr " if (r == NULL) {\n";
- raise_error ();
- pr " }\n";
+ pr " if (r == NULL)\n";
+ pr " return last_error (L, g);\n";
pr "\n"
);
@@ -415,9 +617,8 @@ push_string_list (lua_State *L, char **strs)
lua_newtable (L);
for (i = 0; strs[i] != NULL; ++i) {
- lua_pushinteger (L, i+1 /* because of base 1 arrays */);
lua_pushstring (L, strs[i]);
- lua_settable (L, -3);
+ lua_rawseti (L, -2, i+1 /* because of base 1 arrays */);
}
}
@@ -459,8 +660,78 @@ push_int64 (lua_State *L, int64_t i64)
lua_pushstring (L, s);
}
+static void
+push_int64_array (lua_State *L, const int64_t *array, size_t len)
+{
+ size_t i;
+
+ lua_newtable (L);
+ for (i = 0; i < len; ++i) {
+ push_int64 (L, array[i]);
+ lua_rawseti (L, -2, i+1 /* because of base 1 arrays */);
+ }
+}
+
+";
+
+ (* Code to handle events. *)
+ pr "\
+static uint64_t
+get_event_bitmask (lua_State *L, int index)
+{
+ uint64_t bitmask;
+
+ if (lua_isstring (L, index))
+ return get_event (L, index);
+
+ bitmask = 0;
+
+ lua_pushnil (L);
+ while (lua_next (L, index) != 0) {
+ bitmask |= get_event (L, -1);
+ lua_pop (L, 1); /* pop value */
+ }
+ lua_pop (L, 1); /* pop key */
+
+ return bitmask;
+}
+
+static uint64_t
+get_event (lua_State *L, int index)
+{
+ const char *s;
+
+ s = luaL_checkstring (L, index);
+";
+
+ List.iter (
+ fun (event, i) ->
+ pr " if (strcmp (s, \"%s\") == 0)\n" event;
+ pr " return %d;\n" i
+ ) events;
+
+ pr " return luaL_error (L, \"unknown event name '%%s'\", s);
+}
+
+static void
+push_event (lua_State *L, uint64_t event)
+{
+";
+
+ List.iter (
+ fun (event, i) ->
+ pr " if (event == %d) {\n" i;
+ pr " lua_pushliteral (L, \"%s\");\n" event;
+ pr " return;\n";
+ pr " }\n";
+ ) events;
+
+ pr " abort (); /* should never happen */
+}
+
";
+ (* Code to push structs. *)
let generate_push_struct typ =
pr "static void\n";
pr "push_%s (lua_State *L, struct guestfs_%s *v)\n" typ typ;
@@ -501,9 +772,8 @@ push_int64 (lua_State *L, int64_t i64)
pr "\n";
pr " lua_newtable (L);\n";
pr " for (i = 0; i < v->len; ++i) {\n";
- pr " lua_pushinteger (L, i+1 /* because of base 1 arrays */);\n";
pr " push_%s (L, &v->val[i]);\n" typ;
- pr " lua_settable (L, -3);\n";
+ pr " lua_rawseti (L, -2, i+1 /* because of base 1 arrays */);\n";
pr " }\n";
pr "}\n";
pr "\n"
@@ -525,6 +795,8 @@ static luaL_Reg handle_methods[] = {
{ \"create\", lua_guestfs_create },
{ \"close\", lua_guestfs_close },
{ \"user_cancel\", lua_guestfs_user_cancel },
+ { \"set_event_callback\", lua_guestfs_set_event_callback },
+ { \"delete_event_callback\", lua_guestfs_delete_event_callback },
";
diff --git a/lua/Makefile.am b/lua/Makefile.am
index e2daceee..7fff50b8 100644
--- a/lua/Makefile.am
+++ b/lua/Makefile.am
@@ -55,12 +55,14 @@ TESTS = \
tests/025-create-flags.lua \
tests/027-create-multiple.lua \
tests/030-config.lua \
- tests/070-optargs.lua
+ tests/070-optargs.lua \
+ tests/400-events.lua
if ENABLE_APPLIANCE
TESTS += \
tests/050-lvcreate.lua \
- tests/060-readdir.lua
+ tests/060-readdir.lua \
+ tests/400-progress.lua
endif
EXTRA_DIST += \
@@ -71,7 +73,9 @@ EXTRA_DIST += \
tests/030-config.lua \
tests/050-lvcreate.lua \
tests/060-readdir.lua \
- tests/070-optargs.lua
+ tests/070-optargs.lua \
+ tests/400-events.lua \
+ tests/400-progress.lua
# Custom install rule.
install-data-hook:
diff --git a/lua/examples/guestfs-lua.pod b/lua/examples/guestfs-lua.pod
index c8afb887..83900cae 100644
--- a/lua/examples/guestfs-lua.pod
+++ b/lua/examples/guestfs-lua.pod
@@ -83,6 +83,33 @@ The C<errno> (corresponding to L<guestfs(3)/guestfs_last_errno>).
Note that some errors can also be thrown as plain strings. You
need to check the type.
+=head2 EVENTS
+
+Events can be registered by calling C<set_event_callback>:
+
+ eh = g:set_event_callback (cb, "close")
+
+or to register a single callback for multiple events make the
+second argument a list:
+
+ eh = g:set_event_callback (cb, { "appliance", "library", "trace" })
+
+The callback (C<cb>) is called with the following parameters:
+
+ function cb (g, event, eh, flags, buf, array)
+ -- g is the guestfs handle
+ -- event is a string which is the name of the event that fired
+ -- flags is always zero
+ -- buf is the data buffer (eg. log message etc)
+ -- array is the array of 64 bit ints (eg. progress bar status etc)
+ ...
+ end
+
+You can also remove a callback using the event handle (C<eh>) that was
+returned when you registered the callback:
+
+ g:delete_event_callback (eh)
+
=head1 EXAMPLE 1: CREATE A DISK IMAGE
@EXAMPLE1@
diff --git a/lua/tests/027-create-multiple.lua b/lua/tests/027-create-multiple.lua
index 30ce6155..bd6cae56 100755
--- a/lua/tests/027-create-multiple.lua
+++ b/lua/tests/027-create-multiple.lua
@@ -27,15 +27,6 @@ g1:set_path ("1")
g2:set_path ("2")
g3:set_path ("3")
-if g1:get_path () ~= "1" then
- error (string.format ("incorrect path in g1, expected '1', got '%s'",
- g1:get_path ()))
-end
-if g2:get_path () ~= "2" then
- error (string.format ("incorrect path in g2, expected '2', got '%s'",
- g2:get_path ()))
-end
-if g3:get_path () ~= "3" then
- error (string.format ("incorrect path in g3, expected '3', got '%s'",
- g3:get_path ()))
-end
+assert (g1:get_path () == "1", "incorrect path in g1, expected '1'")
+assert (g2:get_path () == "2", "incorrect path in g2, expected '2'")
+assert (g3:get_path () == "3", "incorrect path in g3, expected '3'")
diff --git a/lua/tests/030-config.lua b/lua/tests/030-config.lua
index a1325584..53e47fc8 100755
--- a/lua/tests/030-config.lua
+++ b/lua/tests/030-config.lua
@@ -28,9 +28,7 @@ g:set_autosync (false)
g:set_autosync (true)
g:set_path (".")
-if g:get_path () ~= "." then
- error ()
-end
+assert (g:get_path () == ".")
g:add_drive ("/dev/null")
diff --git a/lua/tests/050-lvcreate.lua b/lua/tests/050-lvcreate.lua
index a9d9920e..3bd95c23 100755
--- a/lua/tests/050-lvcreate.lua
+++ b/lua/tests/050-lvcreate.lua
@@ -35,10 +35,9 @@ g:lvcreate ("LV1", "VG", 200)
g:lvcreate ("LV2", "VG", 200)
local lvs = g:lvs ()
-if table.getn (lvs) ~= 2 or lvs[1] ~= "/dev/VG/LV1" or lvs[2] ~= "/dev/VG/LV2"
-then
- error ("g:lvs returned incorrect result")
-end
+assert (table.getn (lvs) == 2 and
+ lvs[1] == "/dev/VG/LV1" and lvs[2] == "/dev/VG/LV2",
+ "g:lvs returned incorrect result")
g:shutdown ()
diff --git a/lua/tests/060-readdir.lua b/lua/tests/060-readdir.lua
index dd060840..07e8e3ba 100755
--- a/lua/tests/060-readdir.lua
+++ b/lua/tests/060-readdir.lua
@@ -51,12 +51,8 @@ print_dirs (dirs)
-- Slots 1, 2, 3 contain "." and ".." and "lost+found" respectively.
-if (dirs[4]["name"] ~= "p") then
- error "incorrect name in slot 4"
-end
-if (dirs[5]["name"] ~= "q") then
- error "incorrect name in slot 5"
-end
+assert (dirs[4]["name"] == "p", "incorrect name in slot 4")
+assert (dirs[5]["name"] == "q", "incorrect name in slot 5")
g:shutdown ()
diff --git a/lua/tests/400-events.lua b/lua/tests/400-events.lua
new file mode 100755
index 00000000..c29cc62d
--- /dev/null
+++ b/lua/tests/400-events.lua
@@ -0,0 +1,49 @@
+#!/usr/bin/lua
+-- libguestfs Lua bindings -*- lua -*-
+-- Copyright (C) 2012 Red Hat 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+require "guestfs"
+
+g = Guestfs.create ()
+
+function log_callback (g, event, eh, flags, buf, array)
+ io.write (string.format ("lua event logged: event=%s eh=%d buf='%s'\n",
+ event, eh, buf))
+end
+
+close_invoked = 0
+function close_callback (g, event, eh, flags, buf, array)
+ close_invoked = close_invoked+1
+ log_callback (g, event, eh, flags, buf, array)
+end
+
+-- Register an event callback for all log messages.
+g:set_event_callback (log_callback, { "appliance", "library", "trace" })
+
+-- Register an event callback for the close event.
+g:set_event_callback (close_callback, "close")
+
+-- Make sure we see some messages.
+g:set_trace (true)
+g:set_verbose (true)
+
+-- Do some stuff.
+g:add_drive_ro ("/dev/null")
+
+-- Close the handle. The close callback should be invoked.
+g:close ()
+assert (close_invoked == 1, "close callback was not invoked")
diff --git a/lua/tests/400-progress.lua b/lua/tests/400-progress.lua
new file mode 100755
index 00000000..e0e17ac8
--- /dev/null
+++ b/lua/tests/400-progress.lua
@@ -0,0 +1,44 @@
+#!/usr/bin/lua
+-- libguestfs Lua bindings -*- lua -*-
+-- Copyright (C) 2012 Red Hat 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+require "guestfs"
+
+g = Guestfs.create ()
+
+g:add_drive ("/dev/null")
+g:launch ()
+
+calls = 0
+function cb ()
+ calls = calls+1
+end
+
+eh = g:set_event_callback (cb, "progress")
+assert (g:debug ("progress", {"5"}) == "ok", "debug progress command failed")
+assert (calls > 0, "progress callback was not invoked")
+
+calls = 0
+g:delete_event_callback (eh)
+assert (g:debug ("progress", {"5"}) == "ok", "debug progress command failed")
+assert (calls == 0, "progress callback was invoked when deleted")
+
+g:set_event_callback (cb, "progress")
+assert (g:debug ("progress", {"5"}) == "ok", "debug progress command failed")
+assert (calls > 0, "progress callback was not invoked")
+
+g:close ()