1#!/usr/bin/python3 2# -*- coding: utf-8 -*- 3# 4# Copyright © 2018 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 glib-mkenums utility.""" 22 23import collections 24import os 25import shutil 26import subprocess 27import sys 28import tempfile 29import textwrap 30import unittest 31 32import taptestrunner 33 34 35Result = collections.namedtuple("Result", ("info", "out", "err", "subs")) 36 37 38class TestMkenums(unittest.TestCase): 39 """Integration test for running glib-mkenums. 40 41 This can be run when installed or uninstalled. When uninstalled, it 42 requires G_TEST_BUILDDIR and G_TEST_SRCDIR to be set. 43 44 The idea with this test harness is to test the glib-mkenums utility, its 45 handling of command line arguments, its exit statuses, and its handling of 46 various C source codes. In future we could split the core glib-mkenums 47 parsing and generation code out into a library and unit test that, and 48 convert this test to just check command line behaviour. 49 """ 50 51 # Track the cwd, we want to back out to that to clean up our tempdir 52 cwd = "" 53 rspfile = False 54 55 def setUp(self): 56 self.timeout_seconds = 10 # seconds per test 57 self.tmpdir = tempfile.TemporaryDirectory() 58 self.cwd = os.getcwd() 59 os.chdir(self.tmpdir.name) 60 print("tmpdir:", self.tmpdir.name) 61 if "G_TEST_BUILDDIR" in os.environ: 62 self.__mkenums = os.path.join( 63 os.environ["G_TEST_BUILDDIR"], "..", "glib-mkenums" 64 ) 65 else: 66 self.__mkenums = shutil.which("glib-mkenums") 67 print("rspfile: {}, mkenums:".format(self.rspfile), self.__mkenums) 68 69 def tearDown(self): 70 os.chdir(self.cwd) 71 self.tmpdir.cleanup() 72 73 def _write_rspfile(self, argv): 74 import shlex 75 76 with tempfile.NamedTemporaryFile( 77 dir=self.tmpdir.name, mode="w", delete=False 78 ) as f: 79 contents = " ".join([shlex.quote(arg) for arg in argv]) 80 print("Response file contains:", contents) 81 f.write(contents) 82 f.flush() 83 return f.name 84 85 def runMkenums(self, *args): 86 if self.rspfile: 87 rspfile = self._write_rspfile(args) 88 args = ["@" + rspfile] 89 argv = [self.__mkenums] 90 91 # shebang lines are not supported on native 92 # Windows consoles 93 if os.name == "nt": 94 argv.insert(0, sys.executable) 95 96 argv.extend(args) 97 print("Running:", argv) 98 99 env = os.environ.copy() 100 env["LC_ALL"] = "C.UTF-8" 101 print("Environment:", env) 102 103 # We want to ensure consistent line endings... 104 info = subprocess.run( 105 argv, 106 timeout=self.timeout_seconds, 107 stdout=subprocess.PIPE, 108 stderr=subprocess.PIPE, 109 env=env, 110 universal_newlines=True, 111 ) 112 info.check_returncode() 113 out = info.stdout.strip() 114 err = info.stderr.strip() 115 116 # Known substitutions for standard boilerplate 117 subs = { 118 "standard_top_comment": "This file is generated by glib-mkenums, do not modify " 119 "it. This code is licensed under the same license as the " 120 "containing project. Note that it links to GLib, so must " 121 "comply with the LGPL linking clauses.", 122 "standard_bottom_comment": "Generated data ends here", 123 } 124 125 result = Result(info, out, err, subs) 126 127 print("Output:", result.out) 128 return result 129 130 def runMkenumsWithTemplate(self, template_contents, *args): 131 with tempfile.NamedTemporaryFile( 132 dir=self.tmpdir.name, suffix=".template", delete=False 133 ) as template_file: 134 # Write out the template. 135 template_file.write(template_contents.encode("utf-8")) 136 print(template_file.name + ":", template_contents) 137 template_file.flush() 138 139 return self.runMkenums("--template", template_file.name, *args) 140 141 def runMkenumsWithAllSubstitutions(self, *args): 142 """Run glib-mkenums with a template which outputs all substitutions.""" 143 template_contents = """ 144/*** BEGIN file-header ***/ 145file-header 146/*** END file-header ***/ 147 148/*** BEGIN file-production ***/ 149file-production 150filename: @filename@ 151basename: @basename@ 152/*** END file-production ***/ 153 154/*** BEGIN enumeration-production ***/ 155enumeration-production 156EnumName: @EnumName@ 157enum_name: @enum_name@ 158ENUMNAME: @ENUMNAME@ 159ENUMSHORT: @ENUMSHORT@ 160ENUMPREFIX: @ENUMPREFIX@ 161enumsince: @enumsince@ 162type: @type@ 163Type: @Type@ 164TYPE: @TYPE@ 165/*** END enumeration-production ***/ 166 167/*** BEGIN value-header ***/ 168value-header 169EnumName: @EnumName@ 170enum_name: @enum_name@ 171ENUMNAME: @ENUMNAME@ 172ENUMSHORT: @ENUMSHORT@ 173ENUMPREFIX: @ENUMPREFIX@ 174enumsince: @enumsince@ 175type: @type@ 176Type: @Type@ 177TYPE: @TYPE@ 178/*** END value-header ***/ 179 180/*** BEGIN value-production ***/ 181value-production 182VALUENAME: @VALUENAME@ 183valuenick: @valuenick@ 184valuenum: @valuenum@ 185type: @type@ 186Type: @Type@ 187TYPE: @TYPE@ 188/*** END value-production ***/ 189 190/*** BEGIN value-tail ***/ 191value-tail 192EnumName: @EnumName@ 193enum_name: @enum_name@ 194ENUMNAME: @ENUMNAME@ 195ENUMSHORT: @ENUMSHORT@ 196ENUMPREFIX: @ENUMPREFIX@ 197enumsince: @enumsince@ 198type: @type@ 199Type: @Type@ 200TYPE: @TYPE@ 201/*** END value-tail ***/ 202 203/*** BEGIN comment ***/ 204comment 205comment: @comment@ 206/*** END comment ***/ 207 208/*** BEGIN file-tail ***/ 209file-tail 210/*** END file-tail ***/ 211""" 212 return self.runMkenumsWithTemplate(template_contents, *args) 213 214 def runMkenumsWithHeader(self, h_contents, encoding="utf-8"): 215 with tempfile.NamedTemporaryFile( 216 dir=self.tmpdir.name, suffix=".h", delete=False 217 ) as h_file: 218 # Write out the header to be scanned. 219 h_file.write(h_contents.encode(encoding)) 220 print(h_file.name + ":", h_contents) 221 h_file.flush() 222 223 # Run glib-mkenums with a template which outputs all substitutions. 224 result = self.runMkenumsWithAllSubstitutions(h_file.name) 225 226 # Known substitutions for generated filenames. 227 result.subs.update( 228 {"filename": h_file.name, "basename": os.path.basename(h_file.name)} 229 ) 230 231 return result 232 233 def assertSingleEnum( 234 self, 235 result, 236 enum_name_camel, 237 enum_name_lower, 238 enum_name_upper, 239 enum_name_short, 240 enum_prefix, 241 enum_since, 242 type_lower, 243 type_camel, 244 type_upper, 245 value_name, 246 value_nick, 247 value_num, 248 ): 249 """Assert that out (from runMkenumsWithHeader()) contains a single 250 enum and value matching the given arguments.""" 251 subs = dict( 252 { 253 "enum_name_camel": enum_name_camel, 254 "enum_name_lower": enum_name_lower, 255 "enum_name_upper": enum_name_upper, 256 "enum_name_short": enum_name_short, 257 "enum_prefix": enum_prefix, 258 "enum_since": enum_since, 259 "type_lower": type_lower, 260 "type_camel": type_camel, 261 "type_upper": type_upper, 262 "value_name": value_name, 263 "value_nick": value_nick, 264 "value_num": value_num, 265 }, 266 **result.subs 267 ) 268 269 self.assertEqual( 270 """ 271comment 272comment: {standard_top_comment} 273 274 275file-header 276file-production 277filename: {filename} 278basename: {basename} 279enumeration-production 280EnumName: {enum_name_camel} 281enum_name: {enum_name_lower} 282ENUMNAME: {enum_name_upper} 283ENUMSHORT: {enum_name_short} 284ENUMPREFIX: {enum_prefix} 285enumsince: {enum_since} 286type: {type_lower} 287Type: {type_camel} 288TYPE: {type_upper} 289value-header 290EnumName: {enum_name_camel} 291enum_name: {enum_name_lower} 292ENUMNAME: {enum_name_upper} 293ENUMSHORT: {enum_name_short} 294ENUMPREFIX: {enum_prefix} 295enumsince: {enum_since} 296type: {type_lower} 297Type: {type_camel} 298TYPE: {type_upper} 299value-production 300VALUENAME: {value_name} 301valuenick: {value_nick} 302valuenum: {value_num} 303type: {type_lower} 304Type: {type_camel} 305TYPE: {type_upper} 306value-tail 307EnumName: {enum_name_camel} 308enum_name: {enum_name_lower} 309ENUMNAME: {enum_name_upper} 310ENUMSHORT: {enum_name_short} 311ENUMPREFIX: {enum_prefix} 312enumsince: {enum_since} 313type: {type_lower} 314Type: {type_camel} 315TYPE: {type_upper} 316file-tail 317 318comment 319comment: {standard_bottom_comment} 320""".format( 321 **subs 322 ).strip(), 323 result.out, 324 ) 325 326 def test_help(self): 327 """Test the --help argument.""" 328 result = self.runMkenums("--help") 329 self.assertIn("usage: glib-mkenums", result.out) 330 331 def test_no_args(self): 332 """Test running with no arguments at all.""" 333 result = self.runMkenums() 334 self.assertEqual("", result.err) 335 self.assertEqual( 336 """/* {standard_top_comment} */ 337 338 339/* {standard_bottom_comment} */""".format( 340 **result.subs 341 ), 342 result.out.strip(), 343 ) 344 345 def test_empty_template(self): 346 """Test running with an empty template and no header files.""" 347 result = self.runMkenumsWithTemplate("") 348 self.assertEqual("", result.err) 349 self.assertEqual( 350 """/* {standard_top_comment} */ 351 352 353/* {standard_bottom_comment} */""".format( 354 **result.subs 355 ), 356 result.out.strip(), 357 ) 358 359 def test_no_headers(self): 360 """Test running with a complete template, but no header files.""" 361 result = self.runMkenumsWithAllSubstitutions() 362 self.assertEqual("", result.err) 363 self.assertEqual( 364 """ 365comment 366comment: {standard_top_comment} 367 368 369file-header 370file-tail 371 372comment 373comment: {standard_bottom_comment} 374""".format( 375 **result.subs 376 ).strip(), 377 result.out, 378 ) 379 380 def test_empty_header(self): 381 """Test an empty header.""" 382 result = self.runMkenumsWithHeader("") 383 self.assertEqual("", result.err) 384 self.assertEqual( 385 """ 386comment 387comment: {standard_top_comment} 388 389 390file-header 391file-tail 392 393comment 394comment: {standard_bottom_comment} 395""".format( 396 **result.subs 397 ).strip(), 398 result.out, 399 ) 400 401 def test_enum_name(self): 402 """Test typedefs with an enum and a typedef name. Bug #794506.""" 403 h_contents = """ 404 typedef enum _SomeEnumIdentifier { 405 ENUM_VALUE 406 } SomeEnumIdentifier; 407 """ 408 result = self.runMkenumsWithHeader(h_contents) 409 self.assertEqual("", result.err) 410 self.assertSingleEnum( 411 result, 412 "SomeEnumIdentifier", 413 "some_enum_identifier", 414 "SOME_ENUM_IDENTIFIER", 415 "ENUM_IDENTIFIER", 416 "SOME", 417 "", 418 "enum", 419 "Enum", 420 "ENUM", 421 "ENUM_VALUE", 422 "value", 423 "0", 424 ) 425 426 def test_non_utf8_encoding(self): 427 """Test source files with non-UTF-8 encoding. Bug #785113.""" 428 h_contents = """ 429 /* Copyright © La Peña */ 430 typedef enum { 431 ENUM_VALUE 432 } SomeEnumIdentifier; 433 """ 434 result = self.runMkenumsWithHeader(h_contents, encoding="iso-8859-1") 435 self.assertIn("WARNING: UnicodeWarning: ", result.err) 436 self.assertSingleEnum( 437 result, 438 "SomeEnumIdentifier", 439 "some_enum_identifier", 440 "SOME_ENUM_IDENTIFIER", 441 "ENUM_IDENTIFIER", 442 "SOME", 443 "", 444 "enum", 445 "Enum", 446 "ENUM", 447 "ENUM_VALUE", 448 "value", 449 "0", 450 ) 451 452 def test_reproducible(self): 453 """Test builds are reproducible regardless of file ordering. 454 Bug #691436.""" 455 template_contents = "template" 456 457 h_contents1 = """ 458 typedef enum { 459 FIRST, 460 } Header1; 461 """ 462 463 h_contents2 = """ 464 typedef enum { 465 SECOND, 466 } Header2; 467 """ 468 469 with tempfile.NamedTemporaryFile( 470 dir=self.tmpdir.name, suffix="1.h", delete=False 471 ) as h_file1, tempfile.NamedTemporaryFile( 472 dir=self.tmpdir.name, suffix="2.h", delete=False 473 ) as h_file2: 474 # Write out the headers. 475 h_file1.write(h_contents1.encode("utf-8")) 476 h_file2.write(h_contents2.encode("utf-8")) 477 478 h_file1.flush() 479 h_file2.flush() 480 481 # Run glib-mkenums with the headers in one order, and then again 482 # in another order. 483 result1 = self.runMkenumsWithTemplate( 484 template_contents, h_file1.name, h_file2.name 485 ) 486 self.assertEqual("", result1.err) 487 488 result2 = self.runMkenumsWithTemplate( 489 template_contents, h_file2.name, h_file1.name 490 ) 491 self.assertEqual("", result2.err) 492 493 # The output should be the same. 494 self.assertEqual(result1.out, result2.out) 495 496 def test_no_nick(self): 497 """Test trigraphs with a desc but no nick. Issue #1360.""" 498 h_contents = """ 499 typedef enum { 500 GEGL_SAMPLER_NEAREST = 0, /*< desc="nearest" >*/ 501 } GeglSamplerType; 502 """ 503 result = self.runMkenumsWithHeader(h_contents) 504 self.assertEqual("", result.err) 505 self.assertSingleEnum( 506 result, 507 "GeglSamplerType", 508 "gegl_sampler_type", 509 "GEGL_SAMPLER_TYPE", 510 "SAMPLER_TYPE", 511 "GEGL", 512 "", 513 "enum", 514 "Enum", 515 "ENUM", 516 "GEGL_SAMPLER_NEAREST", 517 "nearest", 518 "0", 519 ) 520 521 def test_filename_basename_in_fhead_ftail(self): 522 template_contents = """ 523/*** BEGIN file-header ***/ 524file-header 525filename: @filename@ 526basename: @basename@ 527/*** END file-header ***/ 528 529/*** BEGIN comment ***/ 530comment 531comment: @comment@ 532/*** END comment ***/ 533 534/*** BEGIN file-tail ***/ 535file-tail 536filename: @filename@ 537basename: @basename@ 538/*** END file-tail ***/""" 539 result = self.runMkenumsWithTemplate(template_contents) 540 self.assertEqual( 541 textwrap.dedent( 542 """ 543 WARNING: @filename@ used in file-header section. 544 WARNING: @basename@ used in file-header section. 545 WARNING: @filename@ used in file-tail section. 546 WARNING: @basename@ used in file-tail section. 547 """ 548 ).strip(), 549 result.err, 550 ) 551 self.assertEqual( 552 """ 553comment 554comment: {standard_top_comment} 555 556 557file-header 558filename: @filename@ 559basename: @basename@ 560file-tail 561filename: @filename@ 562basename: @basename@ 563 564comment 565comment: {standard_bottom_comment} 566""".format( 567 **result.subs 568 ).strip(), 569 result.out, 570 ) 571 572 def test_since(self): 573 """Test user-provided 'since' version handling 574 https://gitlab.gnome.org/GNOME/glib/-/merge_requests/1492""" 575 h_contents = """ 576 typedef enum { /*< since=1.0 >*/ 577 QMI_WMS_MESSAGE_PROTOCOL_CDMA = 0, 578 } QmiWmsMessageProtocol; 579 """ 580 result = self.runMkenumsWithHeader(h_contents) 581 self.assertEqual("", result.err) 582 self.assertSingleEnum( 583 result, 584 "QmiWmsMessageProtocol", 585 "qmi_wms_message_protocol", 586 "QMI_WMS_MESSAGE_PROTOCOL", 587 "WMS_MESSAGE_PROTOCOL", 588 "QMI", 589 "1.0", 590 "enum", 591 "Enum", 592 "ENUM", 593 "QMI_WMS_MESSAGE_PROTOCOL_CDMA", 594 "cdma", 595 "0", 596 ) 597 598 def test_enum_private_public(self): 599 """Test private/public enums. Bug #782162.""" 600 h_contents1 = """ 601 typedef enum { 602 ENUM_VALUE_PUBLIC1, 603 /*< private >*/ 604 ENUM_VALUE_PRIVATE, 605 } SomeEnumA 606 """ 607 608 h_contents2 = """ 609 typedef enum { 610 /*< private >*/ 611 ENUM_VALUE_PRIVATE, 612 /*< public >*/ 613 ENUM_VALUE_PUBLIC2, 614 } SomeEnumB; 615 """ 616 617 result = self.runMkenumsWithHeader(h_contents1) 618 self.maxDiff = None 619 self.assertEqual("", result.err) 620 self.assertSingleEnum( 621 result, 622 "SomeEnumA", 623 "some_enum_a", 624 "SOME_ENUM_A", 625 "ENUM_A", 626 "SOME", 627 "", 628 "enum", 629 "Enum", 630 "ENUM", 631 "ENUM_VALUE_PUBLIC1", 632 "public1", 633 "0", 634 ) 635 result = self.runMkenumsWithHeader(h_contents2) 636 self.assertEqual("", result.err) 637 self.assertSingleEnum( 638 result, 639 "SomeEnumB", 640 "some_enum_b", 641 "SOME_ENUM_B", 642 "ENUM_B", 643 "SOME", 644 "", 645 "enum", 646 "Enum", 647 "ENUM", 648 "ENUM_VALUE_PUBLIC2", 649 "public2", 650 "0", 651 ) 652 653 def test_available_in(self): 654 """Test GLIB_AVAILABLE_ENUMERATOR_IN_2_68 handling 655 https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" 656 h_contents = """ 657 typedef enum { 658 G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_AVAILABLE_ENUMERATOR_IN_2_68 = (1<<2) 659 } GDBusServerFlags; 660 """ 661 result = self.runMkenumsWithHeader(h_contents) 662 self.assertEqual("", result.err) 663 self.assertSingleEnum( 664 result, 665 "GDBusServerFlags", 666 "g_dbus_server_flags", 667 "G_DBUS_SERVER_FLAGS", 668 "DBUS_SERVER_FLAGS", 669 "G", 670 "", 671 "flags", 672 "Flags", 673 "FLAGS", 674 "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", 675 "user", 676 "4", 677 ) 678 679 def test_deprecated_in(self): 680 """Test GLIB_DEPRECATED_ENUMERATOR_IN_2_68 handling 681 https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" 682 h_contents = """ 683 typedef enum { 684 G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_DEPRECATED_ENUMERATOR_IN_2_68 = (1<<2) 685 } GDBusServerFlags; 686 """ 687 result = self.runMkenumsWithHeader(h_contents) 688 self.assertEqual("", result.err) 689 self.assertSingleEnum( 690 result, 691 "GDBusServerFlags", 692 "g_dbus_server_flags", 693 "G_DBUS_SERVER_FLAGS", 694 "DBUS_SERVER_FLAGS", 695 "G", 696 "", 697 "flags", 698 "Flags", 699 "FLAGS", 700 "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", 701 "user", 702 "4", 703 ) 704 705 def test_deprecated_in_for(self): 706 """Test GLIB_DEPRECATED_ENUMERATOR_IN_2_68_FOR() handling 707 https://gitlab.gnome.org/GNOME/glib/-/issues/2327""" 708 h_contents = """ 709 typedef enum { 710 G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER GLIB_DEPRECATED_ENUMERATOR_IN_2_68_FOR(G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER2) = (1<<2) 711 } GDBusServerFlags; 712 """ 713 result = self.runMkenumsWithHeader(h_contents) 714 self.assertEqual("", result.err) 715 self.assertSingleEnum( 716 result, 717 "GDBusServerFlags", 718 "g_dbus_server_flags", 719 "G_DBUS_SERVER_FLAGS", 720 "DBUS_SERVER_FLAGS", 721 "G", 722 "", 723 "flags", 724 "Flags", 725 "FLAGS", 726 "G_DBUS_SERVER_FLAGS_AUTHENTICATION_REQUIRE_SAME_USER", 727 "user", 728 "4", 729 ) 730 731 732class TestRspMkenums(TestMkenums): 733 """Run all tests again in @rspfile mode""" 734 735 rspfile = True 736 737 738if __name__ == "__main__": 739 unittest.main(testRunner=taptestrunner.TAPTestRunner()) 740