| #!/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 textwrap |
| import unittest |
| |
| import taptestrunner |
| |
| |
| 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()) |