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