glib/gio/tests/codegen.py
Matthew Leeds 2a605f6e15 gdbus-codegen: Add call_flags and timeout_msec args
Currently the code generated by gdbus-codegen uses
G_DBUS_CALL_FLAGS_NONE in its D-Bus calls, which occur for each method
defined by the input XML, and for proxy_set_property functions. This
means that if the daemon which implements the methods checks for
G_DBUS_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION and only does interactive
authorization if that flag is present, users of the generated code have
no way to cause the daemon to use interactive authorization (e.g. polkit
dialogs).

If we simply changed the generated code to always use
G_DBUS_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION, its users would have no
way to disallow interactive authorization (except for manually calling
the D-Bus method themselves).

So instead, this commit adds a GDBusCallFlags argument to method call
functions. Since this is an API break which will require changes in
projects using gdbus-codegen code, the change is conditional on the
command line argument --glib-min-version having the value 2.64 or
higher.

The impetus for this change is that I'm changing accountsservice to
properly respect G_DBUS_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION, and
libaccountsservice uses generated code for D-Bus method calls. So
these changes will allow libaccountsservice to continue allowing
interactive authorization, and avoid breaking any users of it which
expect that. See
https://gitlab.freedesktop.org/accountsservice/accountsservice/merge_requests/46

It might make sense to also let GDBusCallFlags be specified for property
set operations, but that is not needed in the case of accountsservice,
and would require significant work and breaking API in multiple places.

Similarly, the generated code currently hard codes -1 as the timeout
value when calling g_dbus_proxy_call*(). Add a timeout_msec argument so
the user of the generated code can specify the timeout as well.

Also, test this new API. In gio/tests/codegen.py we test that the new
arguments are generated if and only of --glib-min-version is used with a
value greater than or equal to 2.64, and in gio/tests/meson.build we
test that the generated code with the new API can be linked against.

The test_unix_fd_list() test also needed modification to continue
working now that we're using gdbus-test-codegen.c with code generated
with --glib-min-version=2.64 in one test.

Finally, update the docs for gdbus-codegen to explain the effect of
using --glib-min-version 2.64, both from this commit and from
"gdbus-codegen: Emit GUnixFDLists if an arg has type `h` w/
min-version".
2020-01-15 09:37:41 -08:00

491 lines
20 KiB
Python

#!/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_version_invalid(self):
"""Test running with an invalid --glib-min-version."""
with self.assertRaises(subprocess.CalledProcessError):
self.runCodegenWithInterface('',
'--output', '/dev/stdout',
'--body',
'--glib-min-version', 'hello mum')
def test_glib_min_version_too_low(self):
"""Test running with a --glib-min-version which is too low (and hence
probably a typo)."""
with self.assertRaises(subprocess.CalledProcessError):
self.runCodegenWithInterface('',
'--output', '/dev/stdout',
'--body',
'--glib-min-version', '2.6')
def test_glib_min_version_major_only(self):
"""Test running with a --glib-min-version which contains only a major version."""
result = self.runCodegenWithInterface('',
'--output', '/dev/stdout',
'--header',
'--glib-min-version', '3')
self.assertEqual('', result.err)
self.assertNotEqual('', result.out.strip())
def test_glib_min_version_with_micro(self):
"""Test running with a --glib-min-version which contains a micro version."""
result = self.runCodegenWithInterface('',
'--output', '/dev/stdout',
'--header',
'--glib-min-version', '2.46.2')
self.assertEqual('', result.err)
self.assertNotEqual('', result.out.strip())
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-version.
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-version.
result = self.runCodegenWithInterface(interface_xml,
'--output', '/dev/stdout',
'--header',
'--glib-min-version', '2.32')
self.assertEqual('', result.err)
self.assertEqual(result.out.strip().count('GUnixFDList'), 6)
# Specify a --glib-min-version ≥ 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-version', '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-version.
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-version.
result = self.runCodegenWithInterface(interface_xml,
'--output', '/dev/stdout',
'--header',
'--glib-min-version', '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-version ≥ 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-version', '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())