blob: 51de0ede43d0eb5f794a99fe00ad6d1f7ca6bb79 [file] [log] [blame]
#!/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())