diff --git a/gio/gdbus-2.0/codegen/codegen.py b/gio/gdbus-2.0/codegen/codegen.py index 119c97d68..3b3afe611 100644 --- a/gio/gdbus-2.0/codegen/codegen.py +++ b/gio/gdbus-2.0/codegen/codegen.py @@ -29,10 +29,11 @@ from . import dbustypes from .utils import print_error LICENSE_STR = '''/* - * Generated by gdbus-codegen {!s} from {!s}. DO NOT EDIT. + * This file is generated by gdbus-codegen, do not modify it. * * The license of this code is the same as for the D-Bus interface description - * it was derived from. + * it was derived from. Note that it links to GLib, so must comply with the + * LGPL linking clauses. */\n''' def generate_namespace(namespace): diff --git a/gio/gdbus-2.0/codegen/codegen_main.py b/gio/gdbus-2.0/codegen/codegen_main.py index 7683f0af5..e77ad5569 100644 --- a/gio/gdbus-2.0/codegen/codegen_main.py +++ b/gio/gdbus-2.0/codegen/codegen_main.py @@ -149,7 +149,7 @@ def apply_annotations(iface_list, annotation_list): def codegen_main(): arg_parser = argparse.ArgumentParser(description='D-Bus code and documentation generator') - arg_parser.add_argument('files', metavar='FILE', nargs='*', + arg_parser.add_argument('files', metavar='FILE', nargs='+', help='D-Bus introspection XML file') arg_parser.add_argument('--xml-files', metavar='FILE', action='append', default=[], help=argparse.SUPPRESS) diff --git a/gio/tests/codegen.py b/gio/tests/codegen.py new file mode 100644 index 000000000..bffe6f342 --- /dev/null +++ b/gio/tests/codegen.py @@ -0,0 +1,367 @@ +#!/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) + + +if __name__ == '__main__': + unittest.main(testRunner=taptestrunner.TAPTestRunner()) diff --git a/gio/tests/meson.build b/gio/tests/meson.build index a623f7881..623daae75 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -87,6 +87,10 @@ test_extra_programs = { 'gsubprocess-testprog' : {}, } +python_tests = [ + 'codegen.py', +] + test_env = environment() test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) @@ -714,5 +718,42 @@ foreach program_name, extra_args : test_extra_programs ) endforeach +foreach test_name : python_tests + test( + test_name, + python, + args: ['-B', files(test_name)], + env: test_env, + suite: ['gio', 'no-valgrind'], + ) + + if installed_tests_enabled + install_data( + files(test_name), + install_dir: installed_tests_execdir, + install_mode: 'rwxr-xr-x', + ) + + test_conf = configuration_data() + test_conf.set('installed_tests_dir', installed_tests_execdir) + test_conf.set('program', test_name) + test_conf.set('env', '') + configure_file( + input: installed_tests_template_tap, + output: test_name + '.test', + install_dir: installed_tests_metadir, + configuration: test_conf, + ) + endif +endforeach + +# TAP test runner for Python tests +if installed_tests_enabled + install_data( + files('taptestrunner.py'), + install_dir: installed_tests_execdir, + ) +endif + # FIXME: subdir('services') subdir('modules') diff --git a/gio/tests/taptestrunner.py b/gio/tests/taptestrunner.py new file mode 100644 index 000000000..261496197 --- /dev/null +++ b/gio/tests/taptestrunner.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# coding=utf-8 + +# Copyright (c) 2015 Remko Tronçon (https://el-tramo.be) +# Copied from https://github.com/remko/pycotap/ +# +# Released under the MIT license +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import unittest +import sys +import base64 +from io import StringIO + +# Log modes +class LogMode(object) : + LogToError, LogToDiagnostics, LogToYAML, LogToAttachment = range(4) + + +class TAPTestResult(unittest.TestResult): + def __init__(self, output_stream, error_stream, message_log, test_output_log): + super(TAPTestResult, self).__init__(self, output_stream) + self.output_stream = output_stream + self.error_stream = error_stream + self.orig_stdout = None + self.orig_stderr = None + self.message = None + self.test_output = None + self.message_log = message_log + self.test_output_log = test_output_log + self.output_stream.write("TAP version 13\n") + self._set_streams() + + def printErrors(self): + self.print_raw("1..%d\n" % self.testsRun) + self._reset_streams() + + def _set_streams(self): + self.orig_stdout = sys.stdout + self.orig_stderr = sys.stderr + if self.message_log == LogMode.LogToError: + self.message = self.error_stream + else: + self.message = StringIO() + if self.test_output_log == LogMode.LogToError: + self.test_output = self.error_stream + else: + self.test_output = StringIO() + + if self.message_log == self.test_output_log: + self.test_output = self.message + sys.stdout = sys.stderr = self.test_output + + def _reset_streams(self): + sys.stdout = self.orig_stdout + sys.stderr = self.orig_stderr + + + def print_raw(self, text): + self.output_stream.write(text) + self.output_stream.flush() + + def print_result(self, result, test, directive = None): + self.output_stream.write("%s %d %s" % (result, self.testsRun, test.id())) + if directive: + self.output_stream.write(" # " + directive) + self.output_stream.write("\n") + self.output_stream.flush() + + def ok(self, test, directive = None): + self.print_result("ok", test, directive) + + def not_ok(self, test): + self.print_result("not ok", test) + + def startTest(self, test): + super(TAPTestResult, self).startTest(test) + + def stopTest(self, test): + super(TAPTestResult, self).stopTest(test) + if self.message_log == self.test_output_log: + logs = [(self.message_log, self.message, "output")] + else: + logs = [ + (self.test_output_log, self.test_output, "test_output"), + (self.message_log, self.message, "message") + ] + for log_mode, log, log_name in logs: + if log_mode != LogMode.LogToError: + output = log.getvalue() + if len(output): + if log_mode == LogMode.LogToYAML: + self.print_raw(" ---\n") + self.print_raw(" " + log_name + ": |\n") + self.print_raw(" " + output.rstrip().replace("\n", "\n ") + "\n") + self.print_raw(" ...\n") + elif log_mode == LogMode.LogToAttachment: + self.print_raw(" ---\n") + self.print_raw(" " + log_name + ":\n") + self.print_raw(" File-Name: " + log_name + ".txt\n") + self.print_raw(" File-Type: text/plain\n") + self.print_raw(" File-Content: " + base64.b64encode(output) + "\n") + self.print_raw(" ...\n") + else: + self.print_raw("# " + output.rstrip().replace("\n", "\n# ") + "\n") + # Truncate doesn't change the current stream position. + # Seek to the beginning to avoid extensions on subsequent writes. + log.seek(0) + log.truncate(0) + + def addSuccess(self, test): + super(TAPTestResult, self).addSuccess(test) + self.ok(test) + + def addError(self, test, err): + super(TAPTestResult, self).addError(test, err) + self.message.write(self.errors[-1][1] + "\n") + self.not_ok(test) + + def addFailure(self, test, err): + super(TAPTestResult, self).addFailure(test, err) + self.message.write(self.failures[-1][1] + "\n") + self.not_ok(test) + + def addSkip(self, test, reason): + super(TAPTestResult, self).addSkip(test, reason) + self.ok(test, "SKIP " + reason) + + def addExpectedFailure(self, test, err): + super(TAPTestResult, self).addExpectedFailure(test, err) + self.ok(test) + + def addUnexpectedSuccess(self, test): + super(TAPTestResult, self).addUnexpectedSuccess(test) + self.message.write("Unexpected success" + "\n") + self.not_ok(test) + + +class TAPTestRunner(object): + def __init__(self, + message_log = LogMode.LogToYAML, + test_output_log = LogMode.LogToDiagnostics, + output_stream = sys.stdout, error_stream = sys.stderr): + self.output_stream = output_stream + self.error_stream = error_stream + self.message_log = message_log + self.test_output_log = test_output_log + + def run(self, test): + result = TAPTestResult( + self.output_stream, + self.error_stream, + self.message_log, + self.test_output_log) + test(result) + result.printErrors() + + return result