#!/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 \n' '#ifdef G_OS_UNIX\n' '# include \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 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 = ''' ''' xml_contents2 = ''' ''' 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 = ''' ''' # 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 = ''' ''' # 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())