| #!/usr/bin/python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright © 2018, 2019 Endless Mobile, Inc. |
| # |
| # This library is free software; you can redistribute it and/or |
| # modify it under the terms of the GNU Lesser General Public |
| # License as published by the Free Software Foundation; either |
| # version 2.1 of the License, or (at your option) any later version. |
| # |
| # This library 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 |
| # Lesser General Public License for more details. |
| # |
| # You should have received a copy of the GNU Lesser General Public |
| # License along with this library; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |
| # MA 02110-1301 USA |
| |
| """Integration tests for gdbus-codegen utility.""" |
| |
| import collections |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import unittest |
| |
| import taptestrunner |
| |
| |
| # Disable line length warnings as wrapping the C code templates would be hard |
| # flake8: noqa: E501 |
| |
| |
| Result = collections.namedtuple("Result", ("info", "out", "err", "subs")) |
| |
| |
| class TestCodegen(unittest.TestCase): |
| """Integration test for running gdbus-codegen. |
| |
| This can be run when installed or uninstalled. When uninstalled, it |
| requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set. |
| |
| The idea with this test harness is to test the gdbus-codegen utility, its |
| handling of command line arguments, its exit statuses, and its handling of |
| various C source codes. In future we could split out tests for the core |
| parsing and generation code of gdbus-codegen into separate unit tests, and |
| just test command line behaviour in this integration test. |
| """ |
| |
| # Track the cwd, we want to back out to that to clean up our tempdir |
| cwd = "" |
| |
| def setUp(self): |
| self.timeout_seconds = 10 # seconds per test |
| self.tmpdir = tempfile.TemporaryDirectory() |
| self.cwd = os.getcwd() |
| os.chdir(self.tmpdir.name) |
| print("tmpdir:", self.tmpdir.name) |
| if "G_TEST_BUILDDIR" in os.environ: |
| self.__codegen = os.path.join( |
| os.environ["G_TEST_BUILDDIR"], |
| "..", |
| "gdbus-2.0", |
| "codegen", |
| "gdbus-codegen", |
| ) |
| else: |
| self.__codegen = shutil.which("gdbus-codegen") |
| print("codegen:", self.__codegen) |
| |
| def tearDown(self): |
| os.chdir(self.cwd) |
| self.tmpdir.cleanup() |
| |
| def runCodegen(self, *args): |
| argv = [self.__codegen] |
| |
| # shebang lines are not supported on native |
| # Windows consoles |
| if os.name == "nt": |
| argv.insert(0, sys.executable) |
| |
| argv.extend(args) |
| print("Running:", argv) |
| |
| env = os.environ.copy() |
| env["LC_ALL"] = "C.UTF-8" |
| print("Environment:", env) |
| |
| # We want to ensure consistent line endings... |
| info = subprocess.run( |
| argv, |
| timeout=self.timeout_seconds, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| env=env, |
| universal_newlines=True, |
| ) |
| info.check_returncode() |
| out = info.stdout.strip() |
| err = info.stderr.strip() |
| |
| # Known substitutions for standard boilerplate |
| subs = { |
| "standard_top_comment": "/*\n" |
| " * This file is generated by gdbus-codegen, do not modify it.\n" |
| " *\n" |
| " * The license of this code is the same as for the D-Bus interface description\n" |
| " * it was derived from. Note that it links to GLib, so must comply with the\n" |
| " * LGPL linking clauses.\n" |
| " */", |
| "standard_config_h_include": "#ifdef HAVE_CONFIG_H\n" |
| '# include "config.h"\n' |
| "#endif", |
| "standard_header_includes": "#include <string.h>\n" |
| "#ifdef G_OS_UNIX\n" |
| "# include <gio/gunixfdlist.h>\n" |
| "#endif", |
| "standard_typedefs_and_helpers": "typedef struct\n" |
| "{\n" |
| " GDBusArgInfo parent_struct;\n" |
| " gboolean use_gvariant;\n" |
| "} _ExtendedGDBusArgInfo;\n" |
| "\n" |
| "typedef struct\n" |
| "{\n" |
| " GDBusMethodInfo parent_struct;\n" |
| " const gchar *signal_name;\n" |
| " gboolean pass_fdlist;\n" |
| "} _ExtendedGDBusMethodInfo;\n" |
| "\n" |
| "typedef struct\n" |
| "{\n" |
| " GDBusSignalInfo parent_struct;\n" |
| " const gchar *signal_name;\n" |
| "} _ExtendedGDBusSignalInfo;\n" |
| "\n" |
| "typedef struct\n" |
| "{\n" |
| " GDBusPropertyInfo parent_struct;\n" |
| " const gchar *hyphen_name;\n" |
| " guint use_gvariant : 1;\n" |
| " guint emits_changed_signal : 1;\n" |
| "} _ExtendedGDBusPropertyInfo;\n" |
| "\n" |
| "typedef struct\n" |
| "{\n" |
| " GDBusInterfaceInfo parent_struct;\n" |
| " const gchar *hyphen_name;\n" |
| "} _ExtendedGDBusInterfaceInfo;\n" |
| "\n" |
| "typedef struct\n" |
| "{\n" |
| " const _ExtendedGDBusPropertyInfo *info;\n" |
| " guint prop_id;\n" |
| " GValue orig_value; /* the value before the change */\n" |
| "} ChangedProperty;\n" |
| "\n" |
| "static void\n" |
| "_changed_property_free (ChangedProperty *data)\n" |
| "{\n" |
| " g_value_unset (&data->orig_value);\n" |
| " g_free (data);\n" |
| "}\n" |
| "\n" |
| "static gboolean\n" |
| "_g_strv_equal0 (gchar **a, gchar **b)\n" |
| "{\n" |
| " gboolean ret = FALSE;\n" |
| " guint n;\n" |
| " if (a == NULL && b == NULL)\n" |
| " {\n" |
| " ret = TRUE;\n" |
| " goto out;\n" |
| " }\n" |
| " if (a == NULL || b == NULL)\n" |
| " goto out;\n" |
| " if (g_strv_length (a) != g_strv_length (b))\n" |
| " goto out;\n" |
| " for (n = 0; a[n] != NULL; n++)\n" |
| " if (g_strcmp0 (a[n], b[n]) != 0)\n" |
| " goto out;\n" |
| " ret = TRUE;\n" |
| "out:\n" |
| " return ret;\n" |
| "}\n" |
| "\n" |
| "static gboolean\n" |
| "_g_variant_equal0 (GVariant *a, GVariant *b)\n" |
| "{\n" |
| " gboolean ret = FALSE;\n" |
| " if (a == NULL && b == NULL)\n" |
| " {\n" |
| " ret = TRUE;\n" |
| " goto out;\n" |
| " }\n" |
| " if (a == NULL || b == NULL)\n" |
| " goto out;\n" |
| " ret = g_variant_equal (a, b);\n" |
| "out:\n" |
| " return ret;\n" |
| "}\n" |
| "\n" |
| "G_GNUC_UNUSED static gboolean\n" |
| "_g_value_equal (const GValue *a, const GValue *b)\n" |
| "{\n" |
| " gboolean ret = FALSE;\n" |
| " g_assert (G_VALUE_TYPE (a) == G_VALUE_TYPE (b));\n" |
| " switch (G_VALUE_TYPE (a))\n" |
| " {\n" |
| " case G_TYPE_BOOLEAN:\n" |
| " ret = (g_value_get_boolean (a) == g_value_get_boolean (b));\n" |
| " break;\n" |
| " case G_TYPE_UCHAR:\n" |
| " ret = (g_value_get_uchar (a) == g_value_get_uchar (b));\n" |
| " break;\n" |
| " case G_TYPE_INT:\n" |
| " ret = (g_value_get_int (a) == g_value_get_int (b));\n" |
| " break;\n" |
| " case G_TYPE_UINT:\n" |
| " ret = (g_value_get_uint (a) == g_value_get_uint (b));\n" |
| " break;\n" |
| " case G_TYPE_INT64:\n" |
| " ret = (g_value_get_int64 (a) == g_value_get_int64 (b));\n" |
| " break;\n" |
| " case G_TYPE_UINT64:\n" |
| " ret = (g_value_get_uint64 (a) == g_value_get_uint64 (b));\n" |
| " break;\n" |
| " case G_TYPE_DOUBLE:\n" |
| " {\n" |
| " /* Avoid -Wfloat-equal warnings by doing a direct bit compare */\n" |
| " gdouble da = g_value_get_double (a);\n" |
| " gdouble db = g_value_get_double (b);\n" |
| " ret = memcmp (&da, &db, sizeof (gdouble)) == 0;\n" |
| " }\n" |
| " break;\n" |
| " case G_TYPE_STRING:\n" |
| " ret = (g_strcmp0 (g_value_get_string (a), g_value_get_string (b)) == 0);\n" |
| " break;\n" |
| " case G_TYPE_VARIANT:\n" |
| " ret = _g_variant_equal0 (g_value_get_variant (a), g_value_get_variant (b));\n" |
| " break;\n" |
| " default:\n" |
| " if (G_VALUE_TYPE (a) == G_TYPE_STRV)\n" |
| " ret = _g_strv_equal0 (g_value_get_boxed (a), g_value_get_boxed (b));\n" |
| " else\n" |
| ' g_critical ("_g_value_equal() does not handle type %s", g_type_name (G_VALUE_TYPE (a)));\n' |
| " break;\n" |
| " }\n" |
| " return ret;\n" |
| "}", |
| } |
| |
| result = Result(info, out, err, subs) |
| |
| print("Output:", result.out) |
| return result |
| |
| def runCodegenWithInterface(self, interface_contents, *args): |
| with tempfile.NamedTemporaryFile( |
| dir=self.tmpdir.name, suffix=".xml", delete=False |
| ) as interface_file: |
| # Write out the interface. |
| interface_file.write(interface_contents.encode("utf-8")) |
| print(interface_file.name + ":", interface_contents) |
| interface_file.flush() |
| |
| return self.runCodegen(interface_file.name, *args) |
| |
| def test_help(self): |
| """Test the --help argument.""" |
| result = self.runCodegen("--help") |
| self.assertIn("usage: gdbus-codegen", result.out) |
| |
| def test_no_args(self): |
| """Test running with no arguments at all.""" |
| with self.assertRaises(subprocess.CalledProcessError): |
| self.runCodegen() |
| |
| def test_empty_interface_header(self): |
| """Test generating a header with an empty interface file.""" |
| result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--header") |
| self.assertEqual("", result.err) |
| self.assertEqual( |
| """{standard_top_comment} |
| |
| #ifndef __STDOUT__ |
| #define __STDOUT__ |
| |
| #include <gio/gio.h> |
| |
| G_BEGIN_DECLS |
| |
| |
| G_END_DECLS |
| |
| #endif /* __STDOUT__ */""".format( |
| **result.subs |
| ), |
| result.out.strip(), |
| ) |
| |
| def test_empty_interface_body(self): |
| """Test generating a body with an empty interface file.""" |
| result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--body") |
| self.assertEqual("", result.err) |
| self.assertEqual( |
| """{standard_top_comment} |
| |
| {standard_config_h_include} |
| |
| #include "stdout.h" |
| |
| {standard_header_includes} |
| |
| {standard_typedefs_and_helpers}""".format( |
| **result.subs |
| ), |
| result.out.strip(), |
| ) |
| |
| def test_reproducible(self): |
| """Test builds are reproducible regardless of file ordering.""" |
| xml_contents1 = """ |
| <node> |
| <interface name="com.acme.Coyote"> |
| <method name="Run"/> |
| <method name="Sleep"/> |
| <method name="Attack"/> |
| <signal name="Surprised"/> |
| <property name="Mood" type="s" access="read"/> |
| </interface> |
| </node> |
| """ |
| |
| xml_contents2 = """ |
| <node> |
| <interface name="org.project.Bar.Frobnicator"> |
| <method name="RandomMethod"/> |
| </interface> |
| </node> |
| """ |
| |
| with tempfile.NamedTemporaryFile( |
| dir=self.tmpdir.name, suffix="1.xml", delete=False |
| ) as xml_file1, tempfile.NamedTemporaryFile( |
| dir=self.tmpdir.name, suffix="2.xml", delete=False |
| ) as xml_file2: |
| # Write out the interfaces. |
| xml_file1.write(xml_contents1.encode("utf-8")) |
| xml_file2.write(xml_contents2.encode("utf-8")) |
| |
| xml_file1.flush() |
| xml_file2.flush() |
| |
| # Repeat this for headers and bodies. |
| for header_or_body in ["--header", "--body"]: |
| # Run gdbus-codegen with the interfaces in one order, and then |
| # again in another order. |
| result1 = self.runCodegen( |
| xml_file1.name, |
| xml_file2.name, |
| "--output", |
| "/dev/stdout", |
| header_or_body, |
| ) |
| self.assertEqual("", result1.err) |
| |
| result2 = self.runCodegen( |
| xml_file2.name, |
| xml_file1.name, |
| "--output", |
| "/dev/stdout", |
| header_or_body, |
| ) |
| self.assertEqual("", result2.err) |
| |
| # The output should be the same. |
| self.assertEqual(result1.out, result2.out) |
| |
| def test_glib_min_required_invalid(self): |
| """Test running with an invalid --glib-min-required.""" |
| with self.assertRaises(subprocess.CalledProcessError): |
| self.runCodegenWithInterface( |
| "", |
| "--output", |
| "/dev/stdout", |
| "--body", |
| "--glib-min-required", |
| "hello mum", |
| ) |
| |
| def test_glib_min_required_too_low(self): |
| """Test running with a --glib-min-required which is too low (and hence |
| probably a typo).""" |
| with self.assertRaises(subprocess.CalledProcessError): |
| self.runCodegenWithInterface( |
| "", "--output", "/dev/stdout", "--body", "--glib-min-required", "2.6" |
| ) |
| |
| def test_glib_min_required_major_only(self): |
| """Test running with a --glib-min-required which contains only a major version.""" |
| result = self.runCodegenWithInterface( |
| "", |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-min-required", |
| "3", |
| "--glib-max-allowed", |
| "3.2", |
| ) |
| self.assertEqual("", result.err) |
| self.assertNotEqual("", result.out.strip()) |
| |
| def test_glib_min_required_with_micro(self): |
| """Test running with a --glib-min-required which contains a micro version.""" |
| result = self.runCodegenWithInterface( |
| "", "--output", "/dev/stdout", "--header", "--glib-min-required", "2.46.2" |
| ) |
| self.assertEqual("", result.err) |
| self.assertNotEqual("", result.out.strip()) |
| |
| def test_glib_max_allowed_too_low(self): |
| """Test running with a --glib-max-allowed which is too low (and hence |
| probably a typo).""" |
| with self.assertRaises(subprocess.CalledProcessError): |
| self.runCodegenWithInterface( |
| "", "--output", "/dev/stdout", "--body", "--glib-max-allowed", "2.6" |
| ) |
| |
| def test_glib_max_allowed_major_only(self): |
| """Test running with a --glib-max-allowed which contains only a major version.""" |
| result = self.runCodegenWithInterface( |
| "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "3" |
| ) |
| self.assertEqual("", result.err) |
| self.assertNotEqual("", result.out.strip()) |
| |
| def test_glib_max_allowed_with_micro(self): |
| """Test running with a --glib-max-allowed which contains a micro version.""" |
| result = self.runCodegenWithInterface( |
| "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "2.46.2" |
| ) |
| self.assertEqual("", result.err) |
| self.assertNotEqual("", result.out.strip()) |
| |
| def test_glib_max_allowed_unstable(self): |
| """Test running with a --glib-max-allowed which is unstable. It should |
| be rounded up to the next stable version number, and hence should not |
| end up less than --glib-min-required.""" |
| result = self.runCodegenWithInterface( |
| "", |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-max-allowed", |
| "2.63", |
| "--glib-min-required", |
| "2.64", |
| ) |
| self.assertEqual("", result.err) |
| self.assertNotEqual("", result.out.strip()) |
| |
| def test_glib_max_allowed_less_than_min_required(self): |
| """Test running with a --glib-max-allowed which is less than |
| --glib-min-required.""" |
| with self.assertRaises(subprocess.CalledProcessError): |
| self.runCodegenWithInterface( |
| "", |
| "--output", |
| "/dev/stdout", |
| "--body", |
| "--glib-max-allowed", |
| "2.62", |
| "--glib-min-required", |
| "2.64", |
| ) |
| |
| def test_unix_fd_types_and_annotations(self): |
| """Test an interface with `h` arguments, no annotation, and GLib < 2.64. |
| |
| See issue #1726. |
| """ |
| interface_xml = """ |
| <node> |
| <interface name="FDPassing"> |
| <method name="HelloFD"> |
| <annotation name="org.gtk.GDBus.C.UnixFD" value="1"/> |
| <arg name="greeting" direction="in" type="s"/> |
| <arg name="response" direction="out" type="s"/> |
| </method> |
| <method name="NoAnnotation"> |
| <arg name="greeting" direction="in" type="h"/> |
| <arg name="greeting_locale" direction="in" type="s"/> |
| <arg name="response" direction="out" type="h"/> |
| <arg name="response_locale" direction="out" type="s"/> |
| </method> |
| <method name="NoAnnotationNested"> |
| <arg name="files" type="a{sh}" direction="in"/> |
| </method> |
| </interface> |
| </node>""" |
| |
| # Try without specifying --glib-min-required. |
| result = self.runCodegenWithInterface( |
| interface_xml, "--output", "/dev/stdout", "--header" |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GUnixFDList"), 6) |
| |
| # Specify an old --glib-min-required. |
| result = self.runCodegenWithInterface( |
| interface_xml, |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-min-required", |
| "2.32", |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GUnixFDList"), 6) |
| |
| # Specify a --glib-min-required ≥ 2.64. There should be more |
| # mentions of `GUnixFDList` now, since the annotation is not needed to |
| # trigger its use. |
| result = self.runCodegenWithInterface( |
| interface_xml, |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-min-required", |
| "2.64", |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GUnixFDList"), 18) |
| |
| def test_call_flags_and_timeout_method_args(self): |
| """Test that generated method call functions have @call_flags and |
| @timeout_msec args if and only if GLib >= 2.64. |
| """ |
| interface_xml = """ |
| <node> |
| <interface name="org.project.UsefulInterface"> |
| <method name="UsefulMethod"/> |
| </interface> |
| </node>""" |
| |
| # Try without specifying --glib-min-required. |
| result = self.runCodegenWithInterface( |
| interface_xml, "--output", "/dev/stdout", "--header" |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0) |
| self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0) |
| |
| # Specify an old --glib-min-required. |
| result = self.runCodegenWithInterface( |
| interface_xml, |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-min-required", |
| "2.32", |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0) |
| self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0) |
| |
| # Specify a --glib-min-required ≥ 2.64. The two arguments should be |
| # present for both the async and sync method call functions. |
| result = self.runCodegenWithInterface( |
| interface_xml, |
| "--output", |
| "/dev/stdout", |
| "--header", |
| "--glib-min-required", |
| "2.64", |
| ) |
| self.assertEqual("", result.err) |
| self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 2) |
| self.assertEqual(result.out.strip().count("gint timeout_msec,"), 2) |
| |
| |
| if __name__ == "__main__": |
| unittest.main(testRunner=taptestrunner.TAPTestRunner()) |