[glib: 3/4] tests: Add tests for the gdbus-codegen executable



commit 41cabfaa985b013c3223cf40561281714f82fbbb
Author: Philip Withnall <withnall endlessm com>
Date:   Mon Dec 2 14:05:22 2019 +0000

    tests: Add tests for the gdbus-codegen executable
    
    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 <withnall endlessm com>
    
    Helps: #1612

 gio/tests/codegen.py       | 367 +++++++++++++++++++++++++++++++++++++++++++++
 gio/tests/meson.build      |  41 +++++
 gio/tests/taptestrunner.py | 176 ++++++++++++++++++++++
 3 files changed, 584 insertions(+)
---
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 <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)
+
+
+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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]