From 921e6d1245912d85adc20d32c6269ae3fdd20cef Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 2 Dec 2019 14:07:25 +0000 Subject: [PATCH 1/3] codegen: Drop gdbus-codegen version and input filename from output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s not particularly useful to put the gdbus-codegen version or the name of the input file into the output from `gdbus-codegen`, and it makes the output less reproducible. Drop it. Also clarify the licensing. Signed-off-by: Philip Withnall Helps: #1612 --- gio/gdbus-2.0/codegen/codegen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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): From 8f4155c12426b2c042db83dbf4797ca74ae1fb13 Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 2 Dec 2019 14:08:48 +0000 Subject: [PATCH 2/3] codegen: Require at least one interface file to be specified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, running `gdbus-codegen` with no arguments would exit successfully with no output. While technically correct, that seems unhelpful. Require at least one interface file to be specified, so the user gets an error message if they don’t specify any. Signed-off-by: Philip Withnall --- gio/gdbus-2.0/codegen/codegen_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 41cabfaa985b013c3223cf40561281714f82fbbb Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 2 Dec 2019 14:05:22 +0000 Subject: [PATCH 3/3] tests: Add tests for the gdbus-codegen executable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Python-based test wrapper for the `gdbus-codegen` executable, similar to the existing tests for `glib-mkenums` and friends. Add a few basic tests to begin with, but this doesn’t approach anywhere near full coverage. The next step is to move the existing Meson-based `gdbus-codegen` tests from `gio/tests/meson.build` into the Python test suite. Signed-off-by: Philip Withnall Helps: #1612 --- gio/tests/codegen.py | 367 +++++++++++++++++++++++++++++++++++++ gio/tests/meson.build | 41 +++++ gio/tests/taptestrunner.py | 176 ++++++++++++++++++ 3 files changed, 584 insertions(+) create mode 100644 gio/tests/codegen.py create mode 100644 gio/tests/taptestrunner.py 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