• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2# -*- coding: utf-8 -*-
3#
4# Copyright © 2018, 2019 Endless Mobile, Inc.
5#
6# This library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this library; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19# MA  02110-1301  USA
20
21"""Integration tests for gdbus-codegen utility."""
22
23import collections
24import os
25import shutil
26import subprocess
27import sys
28import tempfile
29import unittest
30
31import taptestrunner
32
33
34# Disable line length warnings as wrapping the C code templates would be hard
35# flake8: noqa: E501
36
37
38Result = collections.namedtuple("Result", ("info", "out", "err", "subs"))
39
40
41class TestCodegen(unittest.TestCase):
42    """Integration test for running gdbus-codegen.
43
44    This can be run when installed or uninstalled. When uninstalled, it
45    requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set.
46
47    The idea with this test harness is to test the gdbus-codegen utility, its
48    handling of command line arguments, its exit statuses, and its handling of
49    various C source codes. In future we could split out tests for the core
50    parsing and generation code of gdbus-codegen into separate unit tests, and
51    just test command line behaviour in this integration test.
52    """
53
54    # Track the cwd, we want to back out to that to clean up our tempdir
55    cwd = ""
56
57    def setUp(self):
58        self.timeout_seconds = 10  # seconds per test
59        self.tmpdir = tempfile.TemporaryDirectory()
60        self.cwd = os.getcwd()
61        os.chdir(self.tmpdir.name)
62        print("tmpdir:", self.tmpdir.name)
63        if "G_TEST_BUILDDIR" in os.environ:
64            self.__codegen = os.path.join(
65                os.environ["G_TEST_BUILDDIR"],
66                "..",
67                "gdbus-2.0",
68                "codegen",
69                "gdbus-codegen",
70            )
71        else:
72            self.__codegen = shutil.which("gdbus-codegen")
73        print("codegen:", self.__codegen)
74
75    def tearDown(self):
76        os.chdir(self.cwd)
77        self.tmpdir.cleanup()
78
79    def runCodegen(self, *args):
80        argv = [self.__codegen]
81
82        # shebang lines are not supported on native
83        # Windows consoles
84        if os.name == "nt":
85            argv.insert(0, sys.executable)
86
87        argv.extend(args)
88        print("Running:", argv)
89
90        env = os.environ.copy()
91        env["LC_ALL"] = "C.UTF-8"
92        print("Environment:", env)
93
94        # We want to ensure consistent line endings...
95        info = subprocess.run(
96            argv,
97            timeout=self.timeout_seconds,
98            stdout=subprocess.PIPE,
99            stderr=subprocess.PIPE,
100            env=env,
101            universal_newlines=True,
102        )
103        info.check_returncode()
104        out = info.stdout.strip()
105        err = info.stderr.strip()
106
107        # Known substitutions for standard boilerplate
108        subs = {
109            "standard_top_comment": "/*\n"
110            " * This file is generated by gdbus-codegen, do not modify it.\n"
111            " *\n"
112            " * The license of this code is the same as for the D-Bus interface description\n"
113            " * it was derived from. Note that it links to GLib, so must comply with the\n"
114            " * LGPL linking clauses.\n"
115            " */",
116            "standard_config_h_include": "#ifdef HAVE_CONFIG_H\n"
117            '#  include "config.h"\n'
118            "#endif",
119            "standard_header_includes": "#include <string.h>\n"
120            "#ifdef G_OS_UNIX\n"
121            "#  include <gio/gunixfdlist.h>\n"
122            "#endif",
123            "standard_typedefs_and_helpers": "typedef struct\n"
124            "{\n"
125            "  GDBusArgInfo parent_struct;\n"
126            "  gboolean use_gvariant;\n"
127            "} _ExtendedGDBusArgInfo;\n"
128            "\n"
129            "typedef struct\n"
130            "{\n"
131            "  GDBusMethodInfo parent_struct;\n"
132            "  const gchar *signal_name;\n"
133            "  gboolean pass_fdlist;\n"
134            "} _ExtendedGDBusMethodInfo;\n"
135            "\n"
136            "typedef struct\n"
137            "{\n"
138            "  GDBusSignalInfo parent_struct;\n"
139            "  const gchar *signal_name;\n"
140            "} _ExtendedGDBusSignalInfo;\n"
141            "\n"
142            "typedef struct\n"
143            "{\n"
144            "  GDBusPropertyInfo parent_struct;\n"
145            "  const gchar *hyphen_name;\n"
146            "  guint use_gvariant : 1;\n"
147            "  guint emits_changed_signal : 1;\n"
148            "} _ExtendedGDBusPropertyInfo;\n"
149            "\n"
150            "typedef struct\n"
151            "{\n"
152            "  GDBusInterfaceInfo parent_struct;\n"
153            "  const gchar *hyphen_name;\n"
154            "} _ExtendedGDBusInterfaceInfo;\n"
155            "\n"
156            "typedef struct\n"
157            "{\n"
158            "  const _ExtendedGDBusPropertyInfo *info;\n"
159            "  guint prop_id;\n"
160            "  GValue orig_value; /* the value before the change */\n"
161            "} ChangedProperty;\n"
162            "\n"
163            "static void\n"
164            "_changed_property_free (ChangedProperty *data)\n"
165            "{\n"
166            "  g_value_unset (&data->orig_value);\n"
167            "  g_free (data);\n"
168            "}\n"
169            "\n"
170            "static gboolean\n"
171            "_g_strv_equal0 (gchar **a, gchar **b)\n"
172            "{\n"
173            "  gboolean ret = FALSE;\n"
174            "  guint n;\n"
175            "  if (a == NULL && b == NULL)\n"
176            "    {\n"
177            "      ret = TRUE;\n"
178            "      goto out;\n"
179            "    }\n"
180            "  if (a == NULL || b == NULL)\n"
181            "    goto out;\n"
182            "  if (g_strv_length (a) != g_strv_length (b))\n"
183            "    goto out;\n"
184            "  for (n = 0; a[n] != NULL; n++)\n"
185            "    if (g_strcmp0 (a[n], b[n]) != 0)\n"
186            "      goto out;\n"
187            "  ret = TRUE;\n"
188            "out:\n"
189            "  return ret;\n"
190            "}\n"
191            "\n"
192            "static gboolean\n"
193            "_g_variant_equal0 (GVariant *a, GVariant *b)\n"
194            "{\n"
195            "  gboolean ret = FALSE;\n"
196            "  if (a == NULL && b == NULL)\n"
197            "    {\n"
198            "      ret = TRUE;\n"
199            "      goto out;\n"
200            "    }\n"
201            "  if (a == NULL || b == NULL)\n"
202            "    goto out;\n"
203            "  ret = g_variant_equal (a, b);\n"
204            "out:\n"
205            "  return ret;\n"
206            "}\n"
207            "\n"
208            "G_GNUC_UNUSED static gboolean\n"
209            "_g_value_equal (const GValue *a, const GValue *b)\n"
210            "{\n"
211            "  gboolean ret = FALSE;\n"
212            "  g_assert (G_VALUE_TYPE (a) == G_VALUE_TYPE (b));\n"
213            "  switch (G_VALUE_TYPE (a))\n"
214            "    {\n"
215            "      case G_TYPE_BOOLEAN:\n"
216            "        ret = (g_value_get_boolean (a) == g_value_get_boolean (b));\n"
217            "        break;\n"
218            "      case G_TYPE_UCHAR:\n"
219            "        ret = (g_value_get_uchar (a) == g_value_get_uchar (b));\n"
220            "        break;\n"
221            "      case G_TYPE_INT:\n"
222            "        ret = (g_value_get_int (a) == g_value_get_int (b));\n"
223            "        break;\n"
224            "      case G_TYPE_UINT:\n"
225            "        ret = (g_value_get_uint (a) == g_value_get_uint (b));\n"
226            "        break;\n"
227            "      case G_TYPE_INT64:\n"
228            "        ret = (g_value_get_int64 (a) == g_value_get_int64 (b));\n"
229            "        break;\n"
230            "      case G_TYPE_UINT64:\n"
231            "        ret = (g_value_get_uint64 (a) == g_value_get_uint64 (b));\n"
232            "        break;\n"
233            "      case G_TYPE_DOUBLE:\n"
234            "        {\n"
235            "          /* Avoid -Wfloat-equal warnings by doing a direct bit compare */\n"
236            "          gdouble da = g_value_get_double (a);\n"
237            "          gdouble db = g_value_get_double (b);\n"
238            "          ret = memcmp (&da, &db, sizeof (gdouble)) == 0;\n"
239            "        }\n"
240            "        break;\n"
241            "      case G_TYPE_STRING:\n"
242            "        ret = (g_strcmp0 (g_value_get_string (a), g_value_get_string (b)) == 0);\n"
243            "        break;\n"
244            "      case G_TYPE_VARIANT:\n"
245            "        ret = _g_variant_equal0 (g_value_get_variant (a), g_value_get_variant (b));\n"
246            "        break;\n"
247            "      default:\n"
248            "        if (G_VALUE_TYPE (a) == G_TYPE_STRV)\n"
249            "          ret = _g_strv_equal0 (g_value_get_boxed (a), g_value_get_boxed (b));\n"
250            "        else\n"
251            '          g_critical ("_g_value_equal() does not handle type %s", g_type_name (G_VALUE_TYPE (a)));\n'
252            "        break;\n"
253            "    }\n"
254            "  return ret;\n"
255            "}",
256        }
257
258        result = Result(info, out, err, subs)
259
260        print("Output:", result.out)
261        return result
262
263    def runCodegenWithInterface(self, interface_contents, *args):
264        with tempfile.NamedTemporaryFile(
265            dir=self.tmpdir.name, suffix=".xml", delete=False
266        ) as interface_file:
267            # Write out the interface.
268            interface_file.write(interface_contents.encode("utf-8"))
269            print(interface_file.name + ":", interface_contents)
270            interface_file.flush()
271
272            return self.runCodegen(interface_file.name, *args)
273
274    def test_help(self):
275        """Test the --help argument."""
276        result = self.runCodegen("--help")
277        self.assertIn("usage: gdbus-codegen", result.out)
278
279    def test_no_args(self):
280        """Test running with no arguments at all."""
281        with self.assertRaises(subprocess.CalledProcessError):
282            self.runCodegen()
283
284    def test_empty_interface_header(self):
285        """Test generating a header with an empty interface file."""
286        result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--header")
287        self.assertEqual("", result.err)
288        self.assertEqual(
289            """{standard_top_comment}
290
291#ifndef __STDOUT__
292#define __STDOUT__
293
294#include <gio/gio.h>
295
296G_BEGIN_DECLS
297
298
299G_END_DECLS
300
301#endif /* __STDOUT__ */""".format(
302                **result.subs
303            ),
304            result.out.strip(),
305        )
306
307    def test_empty_interface_body(self):
308        """Test generating a body with an empty interface file."""
309        result = self.runCodegenWithInterface("", "--output", "/dev/stdout", "--body")
310        self.assertEqual("", result.err)
311        self.assertEqual(
312            """{standard_top_comment}
313
314{standard_config_h_include}
315
316#include "stdout.h"
317
318{standard_header_includes}
319
320{standard_typedefs_and_helpers}""".format(
321                **result.subs
322            ),
323            result.out.strip(),
324        )
325
326    def test_reproducible(self):
327        """Test builds are reproducible regardless of file ordering."""
328        xml_contents1 = """
329        <node>
330          <interface name="com.acme.Coyote">
331            <method name="Run"/>
332            <method name="Sleep"/>
333            <method name="Attack"/>
334            <signal name="Surprised"/>
335            <property name="Mood" type="s" access="read"/>
336          </interface>
337        </node>
338        """
339
340        xml_contents2 = """
341        <node>
342          <interface name="org.project.Bar.Frobnicator">
343            <method name="RandomMethod"/>
344          </interface>
345        </node>
346        """
347
348        with tempfile.NamedTemporaryFile(
349            dir=self.tmpdir.name, suffix="1.xml", delete=False
350        ) as xml_file1, tempfile.NamedTemporaryFile(
351            dir=self.tmpdir.name, suffix="2.xml", delete=False
352        ) as xml_file2:
353            # Write out the interfaces.
354            xml_file1.write(xml_contents1.encode("utf-8"))
355            xml_file2.write(xml_contents2.encode("utf-8"))
356
357            xml_file1.flush()
358            xml_file2.flush()
359
360            # Repeat this for headers and bodies.
361            for header_or_body in ["--header", "--body"]:
362                # Run gdbus-codegen with the interfaces in one order, and then
363                # again in another order.
364                result1 = self.runCodegen(
365                    xml_file1.name,
366                    xml_file2.name,
367                    "--output",
368                    "/dev/stdout",
369                    header_or_body,
370                )
371                self.assertEqual("", result1.err)
372
373                result2 = self.runCodegen(
374                    xml_file2.name,
375                    xml_file1.name,
376                    "--output",
377                    "/dev/stdout",
378                    header_or_body,
379                )
380                self.assertEqual("", result2.err)
381
382                # The output should be the same.
383                self.assertEqual(result1.out, result2.out)
384
385    def test_glib_min_required_invalid(self):
386        """Test running with an invalid --glib-min-required."""
387        with self.assertRaises(subprocess.CalledProcessError):
388            self.runCodegenWithInterface(
389                "",
390                "--output",
391                "/dev/stdout",
392                "--body",
393                "--glib-min-required",
394                "hello mum",
395            )
396
397    def test_glib_min_required_too_low(self):
398        """Test running with a --glib-min-required which is too low (and hence
399        probably a typo)."""
400        with self.assertRaises(subprocess.CalledProcessError):
401            self.runCodegenWithInterface(
402                "", "--output", "/dev/stdout", "--body", "--glib-min-required", "2.6"
403            )
404
405    def test_glib_min_required_major_only(self):
406        """Test running with a --glib-min-required which contains only a major version."""
407        result = self.runCodegenWithInterface(
408            "",
409            "--output",
410            "/dev/stdout",
411            "--header",
412            "--glib-min-required",
413            "3",
414            "--glib-max-allowed",
415            "3.2",
416        )
417        self.assertEqual("", result.err)
418        self.assertNotEqual("", result.out.strip())
419
420    def test_glib_min_required_with_micro(self):
421        """Test running with a --glib-min-required which contains a micro version."""
422        result = self.runCodegenWithInterface(
423            "", "--output", "/dev/stdout", "--header", "--glib-min-required", "2.46.2"
424        )
425        self.assertEqual("", result.err)
426        self.assertNotEqual("", result.out.strip())
427
428    def test_glib_max_allowed_too_low(self):
429        """Test running with a --glib-max-allowed which is too low (and hence
430        probably a typo)."""
431        with self.assertRaises(subprocess.CalledProcessError):
432            self.runCodegenWithInterface(
433                "", "--output", "/dev/stdout", "--body", "--glib-max-allowed", "2.6"
434            )
435
436    def test_glib_max_allowed_major_only(self):
437        """Test running with a --glib-max-allowed which contains only a major version."""
438        result = self.runCodegenWithInterface(
439            "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "3"
440        )
441        self.assertEqual("", result.err)
442        self.assertNotEqual("", result.out.strip())
443
444    def test_glib_max_allowed_with_micro(self):
445        """Test running with a --glib-max-allowed which contains a micro version."""
446        result = self.runCodegenWithInterface(
447            "", "--output", "/dev/stdout", "--header", "--glib-max-allowed", "2.46.2"
448        )
449        self.assertEqual("", result.err)
450        self.assertNotEqual("", result.out.strip())
451
452    def test_glib_max_allowed_unstable(self):
453        """Test running with a --glib-max-allowed which is unstable. It should
454        be rounded up to the next stable version number, and hence should not
455        end up less than --glib-min-required."""
456        result = self.runCodegenWithInterface(
457            "",
458            "--output",
459            "/dev/stdout",
460            "--header",
461            "--glib-max-allowed",
462            "2.63",
463            "--glib-min-required",
464            "2.64",
465        )
466        self.assertEqual("", result.err)
467        self.assertNotEqual("", result.out.strip())
468
469    def test_glib_max_allowed_less_than_min_required(self):
470        """Test running with a --glib-max-allowed which is less than
471        --glib-min-required."""
472        with self.assertRaises(subprocess.CalledProcessError):
473            self.runCodegenWithInterface(
474                "",
475                "--output",
476                "/dev/stdout",
477                "--body",
478                "--glib-max-allowed",
479                "2.62",
480                "--glib-min-required",
481                "2.64",
482            )
483
484    def test_unix_fd_types_and_annotations(self):
485        """Test an interface with `h` arguments, no annotation, and GLib < 2.64.
486
487        See issue #1726.
488        """
489        interface_xml = """
490            <node>
491              <interface name="FDPassing">
492                <method name="HelloFD">
493                  <annotation name="org.gtk.GDBus.C.UnixFD" value="1"/>
494                  <arg name="greeting" direction="in" type="s"/>
495                  <arg name="response" direction="out" type="s"/>
496                </method>
497                <method name="NoAnnotation">
498                  <arg name="greeting" direction="in" type="h"/>
499                  <arg name="greeting_locale" direction="in" type="s"/>
500                  <arg name="response" direction="out" type="h"/>
501                  <arg name="response_locale" direction="out" type="s"/>
502                </method>
503                <method name="NoAnnotationNested">
504                  <arg name="files" type="a{sh}" direction="in"/>
505                </method>
506              </interface>
507            </node>"""
508
509        # Try without specifying --glib-min-required.
510        result = self.runCodegenWithInterface(
511            interface_xml, "--output", "/dev/stdout", "--header"
512        )
513        self.assertEqual("", result.err)
514        self.assertEqual(result.out.strip().count("GUnixFDList"), 6)
515
516        # Specify an old --glib-min-required.
517        result = self.runCodegenWithInterface(
518            interface_xml,
519            "--output",
520            "/dev/stdout",
521            "--header",
522            "--glib-min-required",
523            "2.32",
524        )
525        self.assertEqual("", result.err)
526        self.assertEqual(result.out.strip().count("GUnixFDList"), 6)
527
528        # Specify a --glib-min-required ≥ 2.64. There should be more
529        # mentions of `GUnixFDList` now, since the annotation is not needed to
530        # trigger its use.
531        result = self.runCodegenWithInterface(
532            interface_xml,
533            "--output",
534            "/dev/stdout",
535            "--header",
536            "--glib-min-required",
537            "2.64",
538        )
539        self.assertEqual("", result.err)
540        self.assertEqual(result.out.strip().count("GUnixFDList"), 18)
541
542    def test_call_flags_and_timeout_method_args(self):
543        """Test that generated method call functions have @call_flags and
544        @timeout_msec args if and only if GLib >= 2.64.
545        """
546        interface_xml = """
547            <node>
548              <interface name="org.project.UsefulInterface">
549                <method name="UsefulMethod"/>
550              </interface>
551            </node>"""
552
553        # Try without specifying --glib-min-required.
554        result = self.runCodegenWithInterface(
555            interface_xml, "--output", "/dev/stdout", "--header"
556        )
557        self.assertEqual("", result.err)
558        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0)
559        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0)
560
561        # Specify an old --glib-min-required.
562        result = self.runCodegenWithInterface(
563            interface_xml,
564            "--output",
565            "/dev/stdout",
566            "--header",
567            "--glib-min-required",
568            "2.32",
569        )
570        self.assertEqual("", result.err)
571        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 0)
572        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 0)
573
574        # Specify a --glib-min-required ≥ 2.64. The two arguments should be
575        # present for both the async and sync method call functions.
576        result = self.runCodegenWithInterface(
577            interface_xml,
578            "--output",
579            "/dev/stdout",
580            "--header",
581            "--glib-min-required",
582            "2.64",
583        )
584        self.assertEqual("", result.err)
585        self.assertEqual(result.out.strip().count("GDBusCallFlags call_flags,"), 2)
586        self.assertEqual(result.out.strip().count("gint timeout_msec,"), 2)
587
588
589if __name__ == "__main__":
590    unittest.main(testRunner=taptestrunner.TAPTestRunner())
591