Use a squashfs attached as /dev/sdd during the C API tests.
[libguestfs.git] / src / generator.ml
index 009b980..a80e846 100755 (executable)
@@ -116,6 +116,7 @@ type flags =
   | FishAlias of string          (* provide an alias for this cmd in guestfish *)
   | FishAction of string  (* call this function in guestfish *)
   | NotInFish            (* do not export via guestfish *)
+  | NotInDocs            (* do not add this function to documentation *)
 
 let protocol_limit_warning =
   "Because of the message protocol, there is a transfer limit 
@@ -129,15 +130,18 @@ can easily destroy all your data>."
 (* You can supply zero or as many tests as you want per API call.
  *
  * Note that the test environment has 3 block devices, of size 500MB,
- * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc).
+ * 50MB and 10MB (respectively /dev/sda, /dev/sdb, /dev/sdc), and
+ * a fourth squashfs block device with some known files on it (/dev/sdd).
+ *
  * Note for partitioning purposes, the 500MB device has 63 cylinders.
  *
+ * The squashfs block device (/dev/sdd) comes from images/test.sqsh.
+ *
  * To be able to run the tests in a reasonable amount of time,
  * the virtual machine and block devices are reused between tests.
  * So don't try testing kill_subprocess :-x
  *
- * Between each test we blockdev-setrw, umount-all, lvm-remove-all
- * (except InitNone).
+ * Between each test we blockdev-setrw, umount-all, lvm-remove-all.
  *
  * If the appliance is running an older Linux kernel (eg. RHEL 5) then
  * devices are named /dev/hda etc.  To cope with this, the test suite
@@ -154,6 +158,11 @@ can easily destroy all your data>."
  * skipped.  Useful if testing a command which might not work on
  * all variations of libguestfs builds.  A test that has prerequisite
  * of 'Always' is run unconditionally.
+ *
+ * In addition, packagers can skip individual tests by setting the
+ * environment variables:     eg:
+ *   SKIP_TEST_<CMD>_<NUM>=1  SKIP_TEST_COMMAND_3=1  (skips test #3 of command)
+ *   SKIP_TEST_<CMD>=1        SKIP_TEST_ZEROFREE=1   (skips all zerofree tests)
  *)
 type tests = (test_init * test_prereq * test) list
 and test =
@@ -207,6 +216,12 @@ and test_prereq =
      * unimplemented feature.
      *)
   | Disabled
+    (* 'string' is some C code (a function body) that should return
+     * true or false.  The test will run if the code returns true.
+     *)
+  | If of string
+    (* As for 'If' but the test runs _unless_ the code returns true. *)
+  | Unless of string
 
 (* Some initial scenarios for testing. *)
 and test_init =
@@ -243,7 +258,81 @@ and cmd = string list
  * Apart from that, long descriptions are just perldoc paragraphs.
  *)
 
-let non_daemon_functions = [
+(* These test functions are used in the language binding tests. *)
+
+let test_all_args = [
+  String "str";
+  OptString "optstr";
+  StringList "strlist";
+  Bool "b";
+  Int "integer";
+  FileIn "filein";
+  FileOut "fileout";
+]
+
+let test_all_rets = [
+  (* except for RErr, which is tested thoroughly elsewhere *)
+  "test0rint",         RInt "valout";
+  "test0rint64",       RInt64 "valout";
+  "test0rbool",        RBool "valout";
+  "test0rconststring", RConstString "valout";
+  "test0rstring",      RString "valout";
+  "test0rstringlist",  RStringList "valout";
+  "test0rintbool",     RIntBool ("valout", "valout");
+  "test0rpvlist",      RPVList "valout";
+  "test0rvglist",      RVGList "valout";
+  "test0rlvlist",      RLVList "valout";
+  "test0rstat",        RStat "valout";
+  "test0rstatvfs",     RStatVFS "valout";
+  "test0rhashtable",   RHashtable "valout";
+]
+
+let test_functions = [
+  ("test0", (RErr, test_all_args), -1, [NotInFish; NotInDocs],
+   [],
+   "internal test function - do not use",
+   "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+parameter type correctly.
+
+It echos the contents of each parameter to stdout.
+
+You probably don't want to call this function.");
+] @ List.flatten (
+  List.map (
+    fun (name, ret) ->
+      [(name, (ret, [String "val"]), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+It converts string C<val> to the return type.
+
+You probably don't want to call this function.");
+       (name ^ "err", (ret, []), -1, [NotInFish; NotInDocs],
+       [],
+       "internal test function - do not use",
+       "\
+This is an internal test function which is used to test whether
+the automatically generated bindings can handle every possible
+return type correctly.
+
+This function always returns an error.
+
+You probably don't want to call this function.")]
+  ) test_all_rets
+)
+
+(* non_daemon_functions are any functions which don't get processed
+ * in the daemon, eg. functions for setting and getting local
+ * configuration values.
+ *)
+
+let non_daemon_functions = test_functions @ [
   ("launch", (RErr, []), -1, [FishAlias "run"; FishAction "launch"],
    [],
    "launch the qemu subprocess",
@@ -481,6 +570,10 @@ For more information on states, see L<guestfs(3)>.");
 
 ]
 
+(* daemon_functions are any functions which cause some action
+ * to take place in the daemon.
+ *)
+
 let daemon_functions = [
   ("mount", (RErr, [String "device"; String "mountpoint"]), 1, [],
    [InitEmpty, Always, TestOutput (
@@ -570,7 +663,7 @@ should probably use C<guestfs_readdir> instead.");
 
   ("list_devices", (RStringList "devices", []), 7, [],
    [InitEmpty, Always, TestOutputList (
-      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"])],
+      [["list_devices"]], ["/dev/sda"; "/dev/sdb"; "/dev/sdc"; "/dev/sdd"])],
    "list the block devices",
    "\
 List all the block devices.
@@ -913,7 +1006,14 @@ Create a directory named C<path>.");
        ["is_dir"; "/new/foo"]];
     InitBasicFS, Always, TestOutputTrue
       [["mkdir_p"; "/new/foo/bar"];
-       ["is_dir"; "/new"]]],
+       ["is_dir"; "/new"]];
+    (* Regression tests for RHBZ#503133: *)
+    InitBasicFS, Always, TestRun
+      [["mkdir"; "/new"];
+       ["mkdir_p"; "/new"]];
+    InitBasicFS, Always, TestLastFail
+      [["touch"; "/new"];
+       ["mkdir_p"; "/new"]]],
    "create a directory and parents",
    "\
 Create a directory named C<path>, creating any parent directories
@@ -1065,7 +1165,9 @@ information refer to the L<sfdisk(8)> manpage.
 
 To create a single partition occupying the whole disk, you would
 pass C<lines> as a single element list, when the single element being
-the string C<,> (comma).");
+the string C<,> (comma).
+
+See also: C<guestfs_sfdisk_l>, C<guestfs_sfdisk_N>");
 
   ("write_file", (RErr, [String "path"; String "content"; Int "size"]), 44, [ProtocolLimitWarning],
    [InitBasicFS, Always, TestOutput (
@@ -1471,7 +1573,7 @@ This uses the L<blockdev(8)> command.");
   ("upload", (RErr, [FileIn "filename"; String "remotefilename"]), 66, [],
    [InitBasicFS, Always, TestOutput (
       (* Pick a file from cwd which isn't likely to change. *)
-    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
      ["checksum"; "md5"; "/COPYING.LIB"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
    "upload a file from the local machine",
    "\
@@ -1485,7 +1587,7 @@ See also C<guestfs_download>.");
   ("download", (RErr, [String "remotefilename"; FileOut "filename"]), 67, [],
    [InitBasicFS, Always, TestOutput (
       (* Pick a file from cwd which isn't likely to change. *)
-    [["upload"; "COPYING.LIB"; "/COPYING.LIB"];
+    [["upload"; "../COPYING.LIB"; "/COPYING.LIB"];
      ["download"; "/COPYING.LIB"; "testdownload.tmp"];
      ["upload"; "testdownload.tmp"; "/upload"];
      ["checksum"; "md5"; "/upload"]], "e3eda01d9815f8d24aae2dbd89b68b06")],
@@ -1521,7 +1623,10 @@ See also C<guestfs_upload>, C<guestfs_cat>.");
        ["checksum"; "sha384"; "/new"]], "109bb6b5b6d5547c1ce03c7a8bd7d8f80c1cb0957f50c4f7fda04692079917e4f9cad52b878f3d8234e1a170b154b72d");
     InitBasicFS, Always, TestOutput (
       [["write_file"; "/new"; "test\n"; "0"];
-       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123")],
+       ["checksum"; "sha512"; "/new"]], "0e3e75234abc68f4378a86b3f4b32a198ba301845b0cd6e50106e874345700cc6663a86c1ea125dc5e92be17c98f9a0f85ca9d5f595db2012f7cc3571945c123");
+    InitBasicFS, Always, TestOutput (
+      [["mount"; "/dev/sdd"; "/"];
+       ["checksum"; "md5"; "/known-3"]], "46d6ca27ee07cdc6fa99c2e138cc522c")],
    "compute MD5, SHAx or CRC checksum of file",
    "\
 This call computes the MD5, SHAx or CRC checksum of the
@@ -1567,7 +1672,7 @@ The checksum is returned as a printable string.");
 
   ("tar_in", (RErr, [FileIn "tarfile"; String "directory"]), 69, [],
    [InitBasicFS, Always, TestOutput (
-      [["tar_in"; "images/helloworld.tar"; "/"];
+      [["tar_in"; "../images/helloworld.tar"; "/"];
        ["cat"; "/hello"]], "hello\n")],
    "unpack tarfile to directory",
    "\
@@ -1587,7 +1692,7 @@ To download a compressed tarball, use C<guestfs_tgz_out>.");
 
   ("tgz_in", (RErr, [FileIn "tarball"; String "directory"]), 71, [],
    [InitBasicFS, Always, TestOutput (
-      [["tgz_in"; "images/helloworld.tar.gz"; "/"];
+      [["tgz_in"; "../images/helloworld.tar.gz"; "/"];
        ["cat"; "/hello"]], "hello\n")],
    "unpack compressed tarball to directory",
    "\
@@ -1650,22 +1755,25 @@ to find out what you can do.");
 
   ("lvremove", (RErr, [String "device"]), 77, [],
    [InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG/LV1"];
        ["lvs"]], ["/dev/VG/LV2"]);
     InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG"];
        ["lvs"]], []);
     InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["lvremove"; "/dev/VG"];
@@ -1680,15 +1788,17 @@ the VG name, C</dev/VG>.");
 
   ("vgremove", (RErr, [String "vgname"]), 78, [],
    [InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
        ["lvs"]], []);
     InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
@@ -1702,28 +1812,31 @@ group (if any).");
 
   ("pvremove", (RErr, [String "device"]), 79, [],
    [InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["lvs"]], []);
     InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["vgs"]], []);
     InitEmpty, Always, TestOutputList (
-      [["pvcreate"; "/dev/sda"];
-       ["vgcreate"; "VG"; "/dev/sda"];
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["pvcreate"; "/dev/sda1"];
+       ["vgcreate"; "VG"; "/dev/sda1"];
        ["lvcreate"; "LV1"; "VG"; "50"];
        ["lvcreate"; "LV2"; "VG"; "50"];
        ["vgremove"; "VG"];
-       ["pvremove"; "/dev/sda"];
+       ["pvremove"; "/dev/sda1"];
        ["pvs"]], [])],
    "remove an LVM physical volume",
    "\
@@ -1985,6 +2098,188 @@ The returned strings are transcoded to UTF-8.");
 This runs C<hexdump -C> on the given C<path>.  The result is
 the human-readable, canonical hex dump of the file.");
 
+  ("zerofree", (RErr, [String "device"]), 97, [],
+   [InitNone, Always, TestOutput (
+      [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+       ["mkfs"; "ext3"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["write_file"; "/new"; "test file"; "0"];
+       ["umount"; "/dev/sda1"];
+       ["zerofree"; "/dev/sda1"];
+       ["mount"; "/dev/sda1"; "/"];
+       ["cat"; "/new"]], "test file")],
+   "zero unused inodes and disk blocks on ext2/3 filesystem",
+   "\
+This runs the I<zerofree> program on C<device>.  This program
+claims to zero unused inodes and disk blocks on an ext2/3
+filesystem, thus making it possible to compress the filesystem
+more effectively.
+
+You should B<not> run this program if the filesystem is
+mounted.
+
+It is possible that using this program can damage the filesystem
+or data on the filesystem.");
+
+  ("pvresize", (RErr, [String "device"]), 98, [],
+   [],
+   "resize an LVM physical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM physical
+volume to match the new size of the underlying device.");
+
+  ("sfdisk_N", (RErr, [String "device"; Int "n";
+                      Int "cyls"; Int "heads"; Int "sectors";
+                      String "line"]), 99, [DangerWillRobinson],
+   [],
+   "modify a single partition on a block device",
+   "\
+This runs L<sfdisk(8)> option to modify just the single
+partition C<n> (note: C<n> counts from 1).
+
+For other parameters, see C<guestfs_sfdisk>.  You should usually
+pass C<0> for the cyls/heads/sectors parameters.");
+
+  ("sfdisk_l", (RString "partitions", [String "device"]), 100, [],
+   [],
+   "display the partition table",
+   "\
+This displays the partition table on C<device>, in the
+human-readable output of the L<sfdisk(8)> command.  It is
+not intended to be parsed.");
+
+  ("sfdisk_kernel_geometry", (RString "partitions", [String "device"]), 101, [],
+   [],
+   "display the kernel geometry",
+   "\
+This displays the kernel's idea of the geometry of C<device>.
+
+The result is in human-readable format, and not designed to
+be parsed.");
+
+  ("sfdisk_disk_geometry", (RString "partitions", [String "device"]), 102, [],
+   [],
+   "display the disk geometry from the partition table",
+   "\
+This displays the disk geometry of C<device> read from the
+partition table.  Especially in the case where the underlying
+block device has been resized, this can be different from the
+kernel's idea of the geometry (see C<guestfs_sfdisk_kernel_geometry>).
+
+The result is in human-readable format, and not designed to
+be parsed.");
+
+  ("vg_activate_all", (RErr, [Bool "activate"]), 103, [],
+   [],
+   "activate or deactivate all volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in all volume groups.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
+
+This command is the same as running C<vgchange -a y|n>");
+
+  ("vg_activate", (RErr, [Bool "activate"; StringList "volgroups"]), 104, [],
+   [],
+   "activate or deactivate some volume groups",
+   "\
+This command activates or (if C<activate> is false) deactivates
+all logical volumes in the listed volume groups C<volgroups>.
+If activated, then they are made known to the
+kernel, ie. they appear as C</dev/mapper> devices.  If deactivated,
+then those devices disappear.
+
+This command is the same as running C<vgchange -a y|n volgroups...>
+
+Note that if C<volgroups> is an empty list then B<all> volume groups
+are activated or deactivated.");
+
+  ("lvresize", (RErr, [String "device"; Int "mbytes"]), 105, [],
+   [InitNone, Always, TestOutput (
+    [["sfdisk"; "/dev/sda"; "0"; "0"; "0"; ","];
+     ["pvcreate"; "/dev/sda1"];
+     ["vgcreate"; "VG"; "/dev/sda1"];
+     ["lvcreate"; "LV"; "VG"; "10"];
+     ["mkfs"; "ext2"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["write_file"; "/new"; "test content"; "0"];
+     ["umount"; "/"];
+     ["lvresize"; "/dev/VG/LV"; "20"];
+     ["e2fsck_f"; "/dev/VG/LV"];
+     ["resize2fs"; "/dev/VG/LV"];
+     ["mount"; "/dev/VG/LV"; "/"];
+     ["cat"; "/new"]], "test content")],
+   "resize an LVM logical volume",
+   "\
+This resizes (expands or shrinks) an existing LVM logical
+volume to C<mbytes>.  When reducing, data in the reduced part
+is lost.");
+
+  ("resize2fs", (RErr, [String "device"]), 106, [],
+   [], (* lvresize tests this *)
+   "resize an ext2/ext3 filesystem",
+   "\
+This resizes an ext2 or ext3 filesystem to match the size of
+the underlying device.
+
+I<Note:> It is sometimes required that you run C<guestfs_e2fsck_f>
+on the C<device> before calling this command.  For unknown reasons
+C<resize2fs> sometimes gives an error about this and sometimes not.
+In any case, it is always safe to call C<guestfs_e2fsck_f> before
+calling this function.");
+
+  ("find", (RStringList "names", [String "directory"]), 107, [],
+   [InitBasicFS, Always, TestOutputList (
+      [["find"; "/"]], ["lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["touch"; "/a"];
+       ["mkdir"; "/b"];
+       ["touch"; "/b/c"];
+       ["find"; "/"]], ["a"; "b"; "b/c"; "lost+found"]);
+    InitBasicFS, Always, TestOutputList (
+      [["mkdir_p"; "/a/b/c"];
+       ["touch"; "/a/b/c/d"];
+       ["find"; "/a/b/"]], ["c"; "c/d"])],
+   "find all files and directories",
+   "\
+This command lists out all files and directories, recursively,
+starting at C<directory>.  It is essentially equivalent to
+running the shell command C<find directory -print> but some
+post-processing happens on the output, described below.
+
+This returns a list of strings I<without any prefix>.  Thus
+if the directory structure was:
+
+ /tmp/a
+ /tmp/b
+ /tmp/c/d
+
+then the returned list from C<guestfs_find> C</tmp> would be
+4 elements:
+
+ a
+ b
+ c
+ c/d
+
+If C<directory> is not a directory, then this command returns
+an error.
+
+The returned list is sorted.");
+
+  ("e2fsck_f", (RErr, [String "device"]), 108, [],
+   [], (* lvresize tests this *)
+   "check an ext2/ext3 filesystem",
+   "\
+This runs C<e2fsck -p -f device>, ie. runs the ext2/ext3
+filesystem checker on C<device>, noninteractively (C<-p>),
+even if the filesystem appears to be clean (C<-f>).
+
+This command is only needed because of C<guestfs_resize2fs>
+(q.v.).  Normally you should use C<guestfs_fsck>.");
+
 ]
 
 let all_functions = non_daemon_functions @ daemon_functions
@@ -2092,6 +2387,14 @@ let statvfs_cols = [
   "namemax", `Int;
 ]
 
+(* Used for testing language bindings. *)
+type callt =
+  | CallString of string
+  | CallOptString of string option
+  | CallStringList of string list
+  | CallInt of int
+  | CallBool of bool
+
 (* Useful functions.
  * Note we don't want to use any external OCaml libraries which
  * makes this a bit harder than it should be.
@@ -2233,8 +2536,10 @@ let check_functions () =
     fun (name, _, _, _, _, _, _) ->
       if String.length name >= 7 && String.sub name 0 7 = "guestfs" then
        failwithf "function name %s does not need 'guestfs' prefix" name;
-      if contains_uppercase name then
-       failwithf "function name %s should not contain uppercase chars" name;
+      if name = "" then
+       failwithf "function name is empty";
+      if name.[0] < 'a' || name.[0] > 'z' then
+       failwithf "function name %s must start with lowercase a-z" name;
       if String.contains name '-' then
        failwithf "function name %s should not contain '-', use '_' instead."
          name
@@ -2251,9 +2556,13 @@ let check_functions () =
          failwithf "%s param/ret %s should not contain '-' or '_'"
            name n;
        if n = "value" then
-         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" n;
+         failwithf "%s has a param/ret called 'value', which causes conflicts in the OCaml bindings, use something like 'val' or a more descriptive name" name;
+       if n = "int" || n = "char" || n = "short" || n = "long" then
+         failwithf "%s has a param/ret which conflicts with a C type (eg. 'int', 'char' etc.)" name;
+       if n = "i" then
+         failwithf "%s has a param/ret called 'i', which will cause some conflicts in the generated code" name;
        if n = "argv" || n = "args" then
-         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" n
+         failwithf "%s has a param/ret called 'argv' or 'args', which will cause some conflicts in the generated code" name
       in
 
       (match fst style with
@@ -2405,71 +2714,73 @@ let generate_header comment license =
 let rec generate_actions_pod () =
   List.iter (
     fun (shortname, style, _, flags, _, _, longdesc) ->
-      let name = "guestfs_" ^ shortname in
-      pr "=head2 %s\n\n" name;
-      pr " ";
-      generate_prototype ~extern:false ~handle:"handle" name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      (match fst style with
-       | RErr ->
-          pr "This function returns 0 on success or -1 on error.\n\n"
-       | RInt _ ->
-          pr "On error this function returns -1.\n\n"
-       | RInt64 _ ->
-          pr "On error this function returns -1.\n\n"
-       | RBool _ ->
-          pr "This function returns a C truth value on success or -1 on error.\n\n"
-       | RConstString _ ->
-          pr "This function returns a string, or NULL on error.
+      if not (List.mem NotInDocs flags) then (
+       let name = "guestfs_" ^ shortname in
+       pr "=head2 %s\n\n" name;
+       pr " ";
+       generate_prototype ~extern:false ~handle:"handle" name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       (match fst style with
+        | RErr ->
+            pr "This function returns 0 on success or -1 on error.\n\n"
+        | RInt _ ->
+            pr "On error this function returns -1.\n\n"
+        | RInt64 _ ->
+            pr "On error this function returns -1.\n\n"
+        | RBool _ ->
+            pr "This function returns a C truth value on success or -1 on error.\n\n"
+        | RConstString _ ->
+            pr "This function returns a string, or NULL on error.
 The string is owned by the guest handle and must I<not> be freed.\n\n"
-       | RString _ ->
-          pr "This function returns a string, or NULL on error.
+        | RString _ ->
+            pr "This function returns a string, or NULL on error.
 I<The caller must free the returned string after use>.\n\n"
-       | RStringList _ ->
-          pr "This function returns a NULL-terminated array of strings
+        | RStringList _ ->
+            pr "This function returns a NULL-terminated array of strings
 (like L<environ(3)>), or NULL if there was an error.
 I<The caller must free the strings and the array after use>.\n\n"
-       | RIntBool _ ->
-          pr "This function returns a C<struct guestfs_int_bool *>,
+        | RIntBool _ ->
+            pr "This function returns a C<struct guestfs_int_bool *>,
 or NULL if there was an error.
 I<The caller must call C<guestfs_free_int_bool> after use>.\n\n"
-       | RPVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_pv_list *>
+        | RPVList _ ->
+            pr "This function returns a C<struct guestfs_lvm_pv_list *>
 (see E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<guestfs_free_lvm_pv_list> after use>.\n\n"
-       | RVGList _ ->
-          pr "This function returns a C<struct guestfs_lvm_vg_list *>
+        | RVGList _ ->
+            pr "This function returns a C<struct guestfs_lvm_vg_list *>
 (see E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<guestfs_free_lvm_vg_list> after use>.\n\n"
-       | RLVList _ ->
-          pr "This function returns a C<struct guestfs_lvm_lv_list *>
+        | RLVList _ ->
+            pr "This function returns a C<struct guestfs_lvm_lv_list *>
 (see E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<guestfs_free_lvm_lv_list> after use>.\n\n"
-       | RStat _ ->
-          pr "This function returns a C<struct guestfs_stat *>
+        | RStat _ ->
+            pr "This function returns a C<struct guestfs_stat *>
 (see L<stat(2)> and E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<free> after use>.\n\n"
-       | RStatVFS _ ->
-          pr "This function returns a C<struct guestfs_statvfs *>
+        | RStatVFS _ ->
+            pr "This function returns a C<struct guestfs_statvfs *>
 (see L<statvfs(2)> and E<lt>guestfs-structs.hE<gt>),
 or NULL if there was an error.
 I<The caller must call C<free> after use>.\n\n"
-       | RHashtable _ ->
-          pr "This function returns a NULL-terminated array of
+        | RHashtable _ ->
+            pr "This function returns a NULL-terminated array of
 strings, or NULL if there was an error.
 The array of strings will always have length C<2n+1>, where
 C<n> keys and values alternate, followed by the trailing NULL entry.
 I<The caller must free the strings and the array after use>.\n\n"
-      );
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson;
+       );
+       if List.mem ProtocolLimitWarning flags then
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
   ) all_functions_sorted
 
 and generate_structs_pod () =
@@ -3513,10 +3824,7 @@ int main (int argc, char *argv[])
 
   guestfs_set_error_handler (g, print_error, NULL);
 
-  srcdir = getenv (\"srcdir\");
-  if (!srcdir) srcdir = \".\";
-  chdir (srcdir);
-  guestfs_set_path (g, \".\");
+  guestfs_set_path (g, \"../appliance\");
 
   filename = \"test1.img\";
   fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK|O_TRUNC, 0666);
@@ -3602,6 +3910,11 @@ int main (int argc, char *argv[])
     exit (1);
   }
 
+  if (guestfs_add_drive (g, \"../images/test.sqsh\") == -1) {
+    printf (\"guestfs_add_drive %%s FAILED\\n\", filename);
+    exit (1);
+  }
+
   if (guestfs_launch (g) == -1) {
     printf (\"guestfs_launch FAILED\\n\");
     exit (1);
@@ -3666,12 +3979,57 @@ int main (int argc, char *argv[])
 and generate_one_test name i (init, prereq, test) =
   let test_name = sprintf "test_%s_%d" name i in
 
-  pr "static int %s (void)\n" test_name;
-  pr "{\n";
+  pr "\
+static int %s_skip (void)
+{
+  const char *str;
+
+  str = getenv (\"SKIP_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  str = getenv (\"SKIP_TEST_%s\");
+  if (str && strcmp (str, \"1\") == 0) return 1;
+  return 0;
+}
+
+" test_name (String.uppercase test_name) (String.uppercase name);
+
+  (match prereq with
+   | Disabled | Always -> ()
+   | If code | Unless code ->
+       pr "static int %s_prereq (void)\n" test_name;
+       pr "{\n";
+       pr "  %s\n" code;
+       pr "}\n";
+       pr "\n";
+  );
+
+  pr "\
+static int %s (void)
+{
+  if (%s_skip ()) {
+    printf (\"%%s skipped (reason: SKIP_TEST_* variable set)\\n\", \"%s\");
+    return 0;
+  }
+
+" test_name test_name test_name;
 
   (match prereq with
    | Disabled ->
-       pr "  printf (\"%%s skipped (test disabled in generator)\\n\", \"%s\");\n" test_name
+       pr "  printf (\"%%s skipped (reason: test disabled in generator)\\n\", \"%s\");\n" test_name
+   | If _ ->
+       pr "  if (! %s_prereq ()) {\n" test_name;
+       pr "    printf (\"%%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
+   | Unless _ ->
+       pr "  if (%s_prereq ()) {\n" test_name;
+       pr "    printf (\"%%s skipped (reason: test prerequisite)\\n\", \"%s\");\n" test_name;
+       pr "    return 0;\n";
+       pr "  }\n";
+       pr "\n";
+       generate_one_test_body name i test_name init test;
    | Always ->
        generate_one_test_body name i test_name init test
   );
@@ -3683,15 +4041,15 @@ and generate_one_test name i (init, prereq, test) =
 
 and generate_one_test_body name i test_name init test =
   (match init with
-   | InitNone -> ()
+   | InitNone
    | InitEmpty ->
-       pr "  /* InitEmpty for %s (%d) */\n" name i;
+       pr "  /* InitNone|InitEmpty for %s */\n" test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
          ["lvm_remove_all"]]
    | InitBasicFS ->
-       pr "  /* InitBasicFS for %s (%d): create ext2 on /dev/sda1 */\n" name i;
+       pr "  /* InitBasicFS for %s: create ext2 on /dev/sda1 */\n" test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
@@ -3700,8 +4058,8 @@ and generate_one_test_body name i test_name init test =
          ["mkfs"; "ext2"; "/dev/sda1"];
          ["mount"; "/dev/sda1"; "/"]]
    | InitBasicFSonLVM ->
-       pr "  /* InitBasicFSonLVM for %s (%d): create ext2 on /dev/VG/LV */\n"
-        name i;
+       pr "  /* InitBasicFSonLVM for %s: create ext2 on /dev/VG/LV */\n"
+        test_name;
        List.iter (generate_test_command_call test_name)
         [["blockdev_setrw"; "/dev/sda"];
          ["umount_all"];
@@ -4345,9 +4703,12 @@ and generate_fish_completion () =
 #ifdef HAVE_LIBREADLINE
 
 static const char *const commands[] = {
+  BUILTIN_COMMANDS_FOR_COMPLETION,
 ";
 
-  (* Get the commands and sort them, including the aliases. *)
+  (* Get the commands, including the aliases.  They don't need to be
+   * sorted - the generator() function just does a dumb linear search.
+   *)
   let commands =
     List.map (
       fun (name, _, _, flags, _, _, _) ->
@@ -4359,7 +4720,6 @@ static const char *const commands[] = {
        if name <> alias then [name2; alias] else [name2]
     ) all_functions in
   let commands = List.flatten commands in
-  let commands = List.sort compare commands in
 
   List.iter (pr "  \"%s\",\n") commands;
 
@@ -4405,7 +4765,8 @@ char **do_completion (const char *text, int start, int end)
 and generate_fish_actions_pod () =
   let all_functions_sorted =
     List.filter (
-      fun (_, _, _, flags, _, _, _) -> not (List.mem NotInFish flags)
+      fun (_, _, _, flags, _, _, _) ->
+       not (List.mem NotInFish flags || List.mem NotInDocs flags)
     ) all_functions_sorted in
 
   let rex = Str.regexp "C<guestfs_\\([^>]+\\)>" in
@@ -5061,13 +5422,19 @@ DESTROY (g)
       generate_call_args ~handle:"g" (snd style);
       pr "\n";
       pr "      guestfs_h *g;\n";
-      List.iter (
-       function
-       | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
-       | OptString n -> pr "      char *%s;\n" n
-       | StringList n -> pr "      char **%s;\n" n
-       | Bool n -> pr "      int %s;\n" n
-       | Int n -> pr "      int %s;\n" n
+      iteri (
+       fun i ->
+         function
+         | String n | FileIn n | FileOut n -> pr "      char *%s;\n" n
+         | OptString n ->
+             (* http://www.perlmonks.org/?node_id=554277
+              * Note that the implicit handle argument means we have
+              * to add 1 to the ST(x) operator.
+              *)
+             pr "      char *%s = SvOK(ST(%d)) ? SvPV_nolen(ST(%d)) : NULL;\n" n (i+1) (i+1)
+         | StringList n -> pr "      char **%s;\n" n
+         | Bool n -> pr "      int %s;\n" n
+         | Int n -> pr "      int %s;\n" n
       ) (snd style);
 
       let do_cleanups () =
@@ -5333,15 +5700,17 @@ sub new {
    *)
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
-      let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
-      pr "=item ";
-      generate_perl_prototype name style;
-      pr "\n\n";
-      pr "%s\n\n" longdesc;
-      if List.mem ProtocolLimitWarning flags then
-       pr "%s\n\n" protocol_limit_warning;
-      if List.mem DangerWillRobinson flags then
-       pr "%s\n\n" danger_will_robinson
+      if not (List.mem NotInDocs flags) then (
+       let longdesc = replace_str longdesc "C<guestfs_" "C<$h-E<gt>" in
+       pr "=item ";
+       generate_perl_prototype name style;
+       pr "\n\n";
+       pr "%s\n\n" longdesc;
+       if List.mem ProtocolLimitWarning flags then
+         pr "%s\n\n" protocol_limit_warning;
+       if List.mem DangerWillRobinson flags then
+         pr "%s\n\n" danger_will_robinson
+      )
   ) all_functions_sorted;
 
   (* End of file. *)
@@ -5844,43 +6213,45 @@ class GuestFS:
 
   List.iter (
     fun (name, style, _, flags, _, _, longdesc) ->
-      let doc = replace_str longdesc "C<guestfs_" "C<g." in
-      let doc =
-        match fst style with
-       | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
-       | RString _ -> doc
-       | RStringList _ ->
-           doc ^ "\n\nThis function returns a list of strings."
-       | RIntBool _ ->
-           doc ^ "\n\nThis function returns a tuple (int, bool).\n"
-       | RPVList _ ->
-           doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
-       | RVGList _ ->
-           doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
-       | RLVList _ ->
-           doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
-       | RStat _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
-       | RStatVFS _ ->
-           doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
-       | RHashtable _ ->
-           doc ^ "\n\nThis function returns a dictionary." in
-      let doc =
-       if List.mem ProtocolLimitWarning flags then
-         doc ^ "\n\n" ^ protocol_limit_warning
-       else doc in
-      let doc =
-       if List.mem DangerWillRobinson flags then
-         doc ^ "\n\n" ^ danger_will_robinson
-       else doc in
-      let doc = pod2text ~width:60 name doc in
-      let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
-      let doc = String.concat "\n        " doc in
-
       pr "    def %s " name;
       generate_call_args ~handle:"self" (snd style);
       pr ":\n";
-      pr "        u\"\"\"%s\"\"\"\n" doc;
+
+      if not (List.mem NotInDocs flags) then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+          match fst style with
+         | RErr | RInt _ | RInt64 _ | RBool _ | RConstString _
+         | RString _ -> doc
+         | RStringList _ ->
+             doc ^ "\n\nThis function returns a list of strings."
+         | RIntBool _ ->
+             doc ^ "\n\nThis function returns a tuple (int, bool).\n"
+         | RPVList _ ->
+             doc ^ "\n\nThis function returns a list of PVs.  Each PV is represented as a dictionary."
+         | RVGList _ ->
+             doc ^ "\n\nThis function returns a list of VGs.  Each VG is represented as a dictionary."
+         | RLVList _ ->
+             doc ^ "\n\nThis function returns a list of LVs.  Each LV is represented as a dictionary."
+         | RStat _ ->
+             doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the stat structure."
+         | RStatVFS _ ->
+             doc ^ "\n\nThis function returns a dictionary, with keys matching the various fields in the statvfs structure."
+         | RHashtable _ ->
+             doc ^ "\n\nThis function returns a dictionary." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (fun line -> replace_str line "\\" "\\\\") doc in
+       let doc = String.concat "\n        " doc in
+       pr "        u\"\"\"%s\"\"\"\n" doc;
+      );
       pr "        return libguestfsmod.%s " name;
       generate_call_args ~handle:"self._o" (snd style);
       pr "\n";
@@ -5997,7 +6368,7 @@ static VALUE ruby_guestfs_close (VALUE gv)
            pr "    rb_raise (rb_eTypeError, \"expected string for parameter %%s of %%s\",\n";
            pr "              \"%s\", \"%s\");\n" n name
        | OptString n ->
-           pr "  const char *%s = StringValueCStr (%sv);\n" n n
+           pr "  const char *%s = !NIL_P (%sv) ? StringValueCStr (%sv) : NULL;\n" n n n
        | StringList n ->
            pr "  char **%s;" n;
            pr "  {\n";
@@ -6011,7 +6382,8 @@ static VALUE ruby_guestfs_close (VALUE gv)
            pr "    }\n";
            pr "    %s[len] = NULL;\n" n;
            pr "  }\n";
-       | Bool n
+       | Bool n ->
+           pr "  int %s = RTEST (%sv);\n" n n
        | Int n ->
            pr "  int %s = NUM2INT (%sv);\n" n n
       ) (snd style);
@@ -6231,25 +6603,32 @@ public class GuestFS {
 
   List.iter (
     fun (name, style, _, flags, _, shortdesc, longdesc) ->
-      let doc = replace_str longdesc "C<guestfs_" "C<g." in
-      let doc =
-       if List.mem ProtocolLimitWarning flags then
-         doc ^ "\n\n" ^ protocol_limit_warning
-       else doc in
-      let doc =
-       if List.mem DangerWillRobinson flags then
-         doc ^ "\n\n" ^ danger_will_robinson
-       else doc in
-      let doc = pod2text ~width:60 name doc in
-      let doc = String.concat "\n   * " doc in
-
-      pr "  /**\n";
-      pr "   * %s\n" shortdesc;
-      pr "   *\n";
-      pr "   * %s\n" doc;
-      pr "   * @throws LibGuestFSException\n";
-      pr "   */\n";
-      pr "  ";
+      if not (List.mem NotInDocs flags); then (
+       let doc = replace_str longdesc "C<guestfs_" "C<g." in
+       let doc =
+         if List.mem ProtocolLimitWarning flags then
+           doc ^ "\n\n" ^ protocol_limit_warning
+         else doc in
+       let doc =
+         if List.mem DangerWillRobinson flags then
+           doc ^ "\n\n" ^ danger_will_robinson
+         else doc in
+       let doc = pod2text ~width:60 name doc in
+       let doc = List.map (            (* RHBZ#501883 *)
+         function
+         | "" -> "<p>"
+         | nonempty -> nonempty
+       ) doc in
+       let doc = String.concat "\n   * " doc in
+
+       pr "  /**\n";
+       pr "   * %s\n" shortdesc;
+       pr "   * <p>\n";
+       pr "   * %s\n" doc;
+       pr "   * @throws LibGuestFSException\n";
+       pr "   */\n";
+       pr "  ";
+      );
       generate_java_prototype ~public:true ~semicolon:false name style;
       pr "\n";
       pr "  {\n";
@@ -6518,10 +6897,14 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  %s = (*env)->GetStringUTFChars (env, j%s, NULL);\n" n n
+       | OptString n ->
+           (* This is completely undocumented, but Java null becomes
+            * a NULL parameter.
+            *)
+           pr "  %s = j%s ? (*env)->GetStringUTFChars (env, j%s, NULL) : NULL;\n" n n n
        | StringList n ->
            pr "  %s_len = (*env)->GetArrayLength (env, j%s);\n" n n;
            pr "  %s = guestfs_safe_malloc (g, sizeof (char *) * (%s_len+1));\n" n n;
@@ -6545,10 +6928,12 @@ Java_com_redhat_et_libguestfs_GuestFS__1close
       List.iter (
        function
        | String n
-       | OptString n
        | FileIn n
        | FileOut n ->
            pr "  (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
+       | OptString n ->
+           pr "  if (j%s)\n" n;
+           pr "    (*env)->ReleaseStringUTFChars (env, j%s, %s);\n" n n
        | StringList n ->
            pr "  for (i = 0; i < %s_len; ++i) {\n" n;
            pr "    jobject o = (*env)->GetObjectArrayElement (env, j%s, i);\n"
@@ -6869,6 +7254,402 @@ and generate_haskell_prototype ~handle ?(hs = false) style =
   );
   pr ")"
 
+and generate_bindtests () =
+  generate_header CStyle LGPLv2;
+
+  pr "\
+#include <stdio.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include \"guestfs.h\"
+#include \"guestfs_protocol.h\"
+
+#define error guestfs_error
+
+static void
+print_strings (char * const* const argv)
+{
+  int argc;
+
+  printf (\"[\");
+  for (argc = 0; argv[argc] != NULL; ++argc) {
+    if (argc > 0) printf (\", \");
+    printf (\"\\\"%%s\\\"\", argv[argc]);
+  }
+  printf (\"]\\n\");
+}
+
+/* The test0 function prints its parameters to stdout. */
+";
+
+  let test0, tests =
+    match test_functions with
+    | [] -> assert false
+    | test0 :: tests -> test0, tests in
+
+  let () =
+    let (name, style, _, _, _, _, _) = test0 in
+    generate_prototype ~extern:false ~semicolon:false ~newline:true
+      ~handle:"g" ~prefix:"guestfs_" name style;
+    pr "{\n";
+    List.iter (
+      function
+      | String n
+      | FileIn n
+      | FileOut n -> pr "  printf (\"%%s\\n\", %s);\n" n
+      | OptString n -> pr "  printf (\"%%s\\n\", %s ? %s : \"null\");\n" n n
+      | StringList n -> pr "  print_strings (%s);\n" n
+      | Bool n -> pr "  printf (\"%%s\\n\", %s ? \"true\" : \"false\");\n" n
+      | Int n -> pr "  printf (\"%%d\\n\", %s);\n" n
+    ) (snd style);
+    pr "  /* Java changes stdout line buffering so we need this: */\n";
+    pr "  fflush (stdout);\n";
+    pr "  return 0;\n";
+    pr "}\n";
+    pr "\n" in
+
+  List.iter (
+    fun (name, style, _, _, _, _, _) ->
+      if String.sub name (String.length name - 3) 3 <> "err" then (
+       pr "/* Test normal return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       (match fst style with
+        | RErr ->
+            pr "  return 0;\n"
+        | RInt _ ->
+            pr "  int r;\n";
+            pr "  sscanf (val, \"%%d\", &r);\n";
+            pr "  return r;\n"
+        | RInt64 _ ->
+            pr "  int64_t r;\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r);\n";
+            pr "  return r;\n"
+        | RBool _ ->
+            pr "  return strcmp (val, \"true\") == 0;\n"
+        | RConstString _ ->
+            (* Can't return the input string here.  Return a static
+             * string so we ensure we get a segfault if the caller
+             * tries to free it.
+             *)
+            pr "  return \"static string\";\n"
+        | RString _ ->
+            pr "  return strdup (val);\n"
+        | RStringList _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = malloc ((n+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i] = malloc (16);\n";
+            pr "    snprintf (strs[i], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n] = NULL;\n";
+            pr "  return strs;\n"
+        | RIntBool _ ->
+            pr "  struct guestfs_int_bool *r;\n";
+            pr "  r = malloc (sizeof (struct guestfs_int_bool));\n";
+            pr "  sscanf (val, \"%%\" SCNi32, &r->i);\n";
+            pr "  r->b = 0;\n";
+            pr "  return r;\n"
+        | RPVList _ ->
+            pr "  struct guestfs_lvm_pv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_pv_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_pv));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].pv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].pv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RVGList _ ->
+            pr "  struct guestfs_lvm_vg_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_vg_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_vg));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].vg_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].vg_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RLVList _ ->
+            pr "  struct guestfs_lvm_lv_list *r;\n";
+            pr "  int i;\n";
+            pr "  r = malloc (sizeof (struct guestfs_lvm_lv_list));\n";
+            pr "  sscanf (val, \"%%d\", &r->len);\n";
+            pr "  r->val = calloc (r->len, sizeof (struct guestfs_lvm_lv));\n";
+            pr "  for (i = 0; i < r->len; ++i) {\n";
+            pr "    r->val[i].lv_name = malloc (16);\n";
+            pr "    snprintf (r->val[i].lv_name, 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  return r;\n"
+        | RStat _ ->
+            pr "  struct guestfs_stat *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->dev);\n";
+            pr "  return r;\n"
+        | RStatVFS _ ->
+            pr "  struct guestfs_statvfs *r;\n";
+            pr "  r = calloc (1, sizeof (*r));\n";
+            pr "  sscanf (val, \"%%\" SCNi64, &r->bsize);\n";
+            pr "  return r;\n"
+        | RHashtable _ ->
+            pr "  char **strs;\n";
+            pr "  int n, i;\n";
+            pr "  sscanf (val, \"%%d\", &n);\n";
+            pr "  strs = malloc ((n*2+1) * sizeof (char *));\n";
+            pr "  for (i = 0; i < n; ++i) {\n";
+            pr "    strs[i*2] = malloc (16);\n";
+            pr "    strs[i*2+1] = malloc (16);\n";
+            pr "    snprintf (strs[i*2], 16, \"%%d\", i);\n";
+            pr "    snprintf (strs[i*2+1], 16, \"%%d\", i);\n";
+            pr "  }\n";
+            pr "  strs[n*2] = NULL;\n";
+            pr "  return strs;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      ) else (
+       pr "/* Test error return. */\n";
+       generate_prototype ~extern:false ~semicolon:false ~newline:true
+         ~handle:"g" ~prefix:"guestfs_" name style;
+       pr "{\n";
+       pr "  error (g, \"error\");\n";
+       (match fst style with
+        | RErr | RInt _ | RInt64 _ | RBool _ ->
+            pr "  return -1;\n"
+        | RConstString _
+        | RString _ | RStringList _ | RIntBool _
+        | RPVList _ | RVGList _ | RLVList _ | RStat _ | RStatVFS _
+        | RHashtable _ ->
+            pr "  return NULL;\n"
+       );
+       pr "}\n";
+       pr "\n"
+      )
+  ) tests
+
+and generate_ocaml_bindtests () =
+  generate_header OCamlStyle GPLv2;
+
+  pr "\
+let () =
+  let g = Guestfs.create () in
+";
+
+  let mkargs args =
+    String.concat " " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "(Some \"%s\")" s
+       | CallStringList xs ->
+           "[|" ^ String.concat ";" (List.map (sprintf "\"%s\"") xs) ^ "|]"
+       | CallInt i when i >= 0 -> string_of_int i
+       | CallInt i (* when i < 0 *) -> "(" ^ string_of_int i ^ ")"
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "  Guestfs.%s g %s;\n" f (mkargs args)
+  );
+
+  pr "print_endline \"EOF\"\n"
+
+and generate_perl_bindtests () =
+  pr "#!/usr/bin/perl -w\n";
+  generate_header HashStyle GPLv2;
+
+  pr "\
+use strict;
+
+use Sys::Guestfs;
+
+my $g = Sys::Guestfs->new ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "undef"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "$g->%s (%s);\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_python_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+import guestfs
+
+g = guestfs.GuestFS ()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "None"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> if b then "1" else "0"
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s (%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\"\n"
+
+and generate_ruby_bindtests () =
+  generate_header HashStyle GPLv2;
+
+  pr "\
+require 'guestfs'
+
+g = Guestfs::create()
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "nil"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "[" ^ String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "]"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "g.%s(%s)\n" f (mkargs args)
+  );
+
+  pr "print \"EOF\\n\"\n"
+
+and generate_java_bindtests () =
+  generate_header CStyle GPLv2;
+
+  pr "\
+import com.redhat.et.libguestfs.*;
+
+public class Bindtests {
+    public static void main (String[] argv)
+    {
+        try {
+            GuestFS g = new GuestFS ();
+";
+
+  let mkargs args =
+    String.concat ", " (
+      List.map (
+       function
+       | CallString s -> "\"" ^ s ^ "\""
+       | CallOptString None -> "null"
+       | CallOptString (Some s) -> sprintf "\"%s\"" s
+       | CallStringList xs ->
+           "new String[]{" ^
+             String.concat "," (List.map (sprintf "\"%s\"") xs) ^ "}"
+       | CallInt i -> string_of_int i
+       | CallBool b -> string_of_bool b
+      ) args
+    )
+  in
+
+  generate_lang_bindtests (
+    fun f args -> pr "            g.%s (%s);\n" f (mkargs args)
+  );
+
+  pr "
+            System.out.println (\"EOF\");
+        }
+        catch (Exception exn) {
+            System.err.println (exn);
+            System.exit (1);
+        }
+    }
+}
+"
+
+and generate_haskell_bindtests () =
+  () (* XXX Haskell bindings need to be fleshed out. *)
+
+(* Language-independent bindings tests - we do it this way to
+ * ensure there is parity in testing bindings across all languages.
+ *)
+and generate_lang_bindtests call =
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString None;
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "def");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString ""; CallOptString (Some "");
+               CallStringList []; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"; "2"]; CallBool false;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool true;
+               CallInt 0; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-1); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt (-2); CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 1; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 2; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 4095; CallString "123"; CallString "456"];
+  call "test0" [CallString "abc"; CallOptString (Some "def");
+               CallStringList ["1"]; CallBool false;
+               CallInt 0; CallString ""; CallString ""]
+
+  (* XXX Add here tests of the return and error functions. *)
+
 let output_to filename =
   let filename_new = filename ^ ".new" in
   chan := open_out filename_new;
@@ -6926,10 +7707,14 @@ Run it from the top source directory using the command
   generate_daemon_actions ();
   close ();
 
-  let close = output_to "tests.c" in
+  let close = output_to "capitests/tests.c" in
   generate_tests ();
   close ();
 
+  let close = output_to "src/guestfs-bindtests.c" in
+  generate_bindtests ();
+  close ();
+
   let close = output_to "fish/cmds.c" in
   generate_fish_cmds ();
   close ();
@@ -6962,6 +7747,10 @@ Run it from the top source directory using the command
   generate_ocaml_c ();
   close ();
 
+  let close = output_to "ocaml/bindtests.ml" in
+  generate_ocaml_bindtests ();
+  close ();
+
   let close = output_to "perl/Guestfs.xs" in
   generate_perl_xs ();
   close ();
@@ -6970,6 +7759,10 @@ Run it from the top source directory using the command
   generate_perl_pm ();
   close ();
 
+  let close = output_to "perl/bindtests.pl" in
+  generate_perl_bindtests ();
+  close ();
+
   let close = output_to "python/guestfs-py.c" in
   generate_python_c ();
   close ();
@@ -6978,10 +7771,18 @@ Run it from the top source directory using the command
   generate_python_py ();
   close ();
 
+  let close = output_to "python/bindtests.py" in
+  generate_python_bindtests ();
+  close ();
+
   let close = output_to "ruby/ext/guestfs/_guestfs.c" in
   generate_ruby_c ();
   close ();
 
+  let close = output_to "ruby/bindtests.rb" in
+  generate_ruby_bindtests ();
+  close ();
+
   let close = output_to "java/com/redhat/et/libguestfs/GuestFS.java" in
   generate_java_java ();
   close ();
@@ -7010,6 +7811,14 @@ Run it from the top source directory using the command
   generate_java_c ();
   close ();
 
+  let close = output_to "java/Bindtests.java" in
+  generate_java_bindtests ();
+  close ();
+
   let close = output_to "haskell/Guestfs.hs" in
   generate_haskell_hs ();
   close ();
+
+  let close = output_to "haskell/bindtests.hs" in
+  generate_haskell_bindtests ();
+  close ();