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 # Track the cwd, we want to back out to that to clean up our tempdir 51 cwd = '' 52 rspfile = False 53 54 def setUp(self): 55 self.timeout_seconds = 10 # seconds per test 56 self.tmpdir = tempfile.TemporaryDirectory() 57 self.cwd = os.getcwd() 58 os.chdir(self.tmpdir.name) 59 print('tmpdir:', self.tmpdir.name) 60 if 'G_TEST_BUILDDIR' in os.environ: 61 self.__mkenums = \ 62 os.path.join(os.environ['G_TEST_BUILDDIR'], '..', 63 'glib-mkenums') 64 else: 65 self.__mkenums = shutil.which('glib-mkenums') 66 print('rspfile: {}, mkenums:'.format(self.rspfile), self.__mkenums) 67 68 def tearDown(self): 69 os.chdir(self.cwd) 70 self.tmpdir.cleanup() 71 72 def _write_rspfile(self, argv): 73 import shlex 74 with tempfile.NamedTemporaryFile(dir=self.tmpdir.name, mode='w', 75 delete=False) as f: 76 contents = ' '.join([shlex.quote(arg) for arg in argv]) 77 print('Response file contains:', contents) 78 f.write(contents) 79 f.flush() 80 return f.name 81 82 def runMkenums(self, *args): 83 if self.rspfile: 84 rspfile = self._write_rspfile(args) 85 args = ['@' + rspfile] 86 argv = [self.__mkenums] 87 88 # shebang lines are not supported on native 89 # Windows consoles 90 if os.name == 'nt': 91 argv.insert(0, sys.executable) 92 93 argv.extend(args) 94 print('Running:', argv) 95 96 env = os.environ.copy() 97 env['LC_ALL'] = 'C.UTF-8' 98 print('Environment:', env) 99 100 # We want to ensure consistent line endings... 101 info = subprocess.run(argv, timeout=self.timeout_seconds, 102 stdout=subprocess.PIPE, 103 stderr=subprocess.PIPE, 104 env=env, 105 universal_newlines=True) 106 info.check_returncode() 107 out = info.stdout.strip() 108 err = info.stderr.strip() 109 110 # Known substitutions for standard boilerplate 111 subs = { 112 'standard_top_comment': 113 'This file is generated by glib-mkenums, do not modify ' 114 'it. This code is licensed under the same license as the ' 115 'containing project. Note that it links to GLib, so must ' 116 'comply with the LGPL linking clauses.', 117 'standard_bottom_comment': 'Generated data ends here' 118 } 119 120 result = Result(info, out, err, subs) 121 122 print('Output:', result.out) 123 return result 124 125 def runMkenumsWithTemplate(self, template_contents, *args): 126 with tempfile.NamedTemporaryFile(dir=self.tmpdir.name, 127 suffix='.template', 128 delete=False) as template_file: 129 # Write out the template. 130 template_file.write(template_contents.encode('utf-8')) 131 print(template_file.name + ':', template_contents) 132 template_file.flush() 133 134 return self.runMkenums('--template', template_file.name, *args) 135 136 def runMkenumsWithAllSubstitutions(self, *args): 137 '''Run glib-mkenums with a template which outputs all substitutions.''' 138 template_contents = ''' 139/*** BEGIN file-header ***/ 140file-header 141/*** END file-header ***/ 142 143/*** BEGIN file-production ***/ 144file-production 145filename: @filename@ 146basename: @basename@ 147/*** END file-production ***/ 148 149/*** BEGIN enumeration-production ***/ 150enumeration-production 151EnumName: @EnumName@ 152enum_name: @enum_name@ 153ENUMNAME: @ENUMNAME@ 154ENUMSHORT: @ENUMSHORT@ 155ENUMPREFIX: @ENUMPREFIX@ 156type: @type@ 157Type: @Type@ 158TYPE: @TYPE@ 159/*** END enumeration-production ***/ 160 161/*** BEGIN value-header ***/ 162value-header 163EnumName: @EnumName@ 164enum_name: @enum_name@ 165ENUMNAME: @ENUMNAME@ 166ENUMSHORT: @ENUMSHORT@ 167ENUMPREFIX: @ENUMPREFIX@ 168type: @type@ 169Type: @Type@ 170TYPE: @TYPE@ 171/*** END value-header ***/ 172 173/*** BEGIN value-production ***/ 174value-production 175VALUENAME: @VALUENAME@ 176valuenick: @valuenick@ 177valuenum: @valuenum@ 178type: @type@ 179Type: @Type@ 180TYPE: @TYPE@ 181/*** END value-production ***/ 182 183/*** BEGIN value-tail ***/ 184value-tail 185EnumName: @EnumName@ 186enum_name: @enum_name@ 187ENUMNAME: @ENUMNAME@ 188ENUMSHORT: @ENUMSHORT@ 189ENUMPREFIX: @ENUMPREFIX@ 190type: @type@ 191Type: @Type@ 192TYPE: @TYPE@ 193/*** END value-tail ***/ 194 195/*** BEGIN comment ***/ 196comment 197comment: @comment@ 198/*** END comment ***/ 199 200/*** BEGIN file-tail ***/ 201file-tail 202/*** END file-tail ***/ 203''' 204 return self.runMkenumsWithTemplate(template_contents, *args) 205 206 def runMkenumsWithHeader(self, h_contents, encoding='utf-8'): 207 with tempfile.NamedTemporaryFile(dir=self.tmpdir.name, 208 suffix='.h', 209 delete=False) as h_file: 210 # Write out the header to be scanned. 211 h_file.write(h_contents.encode(encoding)) 212 print(h_file.name + ':', h_contents) 213 h_file.flush() 214 215 # Run glib-mkenums with a template which outputs all substitutions. 216 result = self.runMkenumsWithAllSubstitutions(h_file.name) 217 218 # Known substitutions for generated filenames. 219 result.subs.update({ 220 'filename': h_file.name, 221 'basename': os.path.basename(h_file.name), 222 }) 223 224 return result 225 226 def assertSingleEnum(self, result, enum_name_camel, enum_name_lower, 227 enum_name_upper, enum_name_short, enum_prefix, 228 type_lower, type_camel, type_upper, 229 value_name, value_nick, value_num): 230 """Assert that out (from runMkenumsWithHeader()) contains a single 231 enum and value matching the given arguments.""" 232 subs = dict({ 233 'enum_name_camel': enum_name_camel, 234 'enum_name_lower': enum_name_lower, 235 'enum_name_upper': enum_name_upper, 236 'enum_name_short': enum_name_short, 237 'enum_prefix': enum_prefix, 238 'type_lower': type_lower, 239 'type_camel': type_camel, 240 'type_upper': type_upper, 241 'value_name': value_name, 242 'value_nick': value_nick, 243 'value_num': value_num, 244 }, **result.subs) 245 246 self.assertEqual(''' 247comment 248comment: {standard_top_comment} 249 250 251file-header 252file-production 253filename: {filename} 254basename: {basename} 255enumeration-production 256EnumName: {enum_name_camel} 257enum_name: {enum_name_lower} 258ENUMNAME: {enum_name_upper} 259ENUMSHORT: {enum_name_short} 260ENUMPREFIX: {enum_prefix} 261type: {type_lower} 262Type: {type_camel} 263TYPE: {type_upper} 264value-header 265EnumName: {enum_name_camel} 266enum_name: {enum_name_lower} 267ENUMNAME: {enum_name_upper} 268ENUMSHORT: {enum_name_short} 269ENUMPREFIX: {enum_prefix} 270type: {type_lower} 271Type: {type_camel} 272TYPE: {type_upper} 273value-production 274VALUENAME: {value_name} 275valuenick: {value_nick} 276valuenum: {value_num} 277type: {type_lower} 278Type: {type_camel} 279TYPE: {type_upper} 280value-tail 281EnumName: {enum_name_camel} 282enum_name: {enum_name_lower} 283ENUMNAME: {enum_name_upper} 284ENUMSHORT: {enum_name_short} 285ENUMPREFIX: {enum_prefix} 286type: {type_lower} 287Type: {type_camel} 288TYPE: {type_upper} 289file-tail 290 291comment 292comment: {standard_bottom_comment} 293'''.format(**subs).strip(), result.out) 294 295 def test_help(self): 296 """Test the --help argument.""" 297 result = self.runMkenums('--help') 298 self.assertIn('usage: glib-mkenums', result.out) 299 300 def test_no_args(self): 301 """Test running with no arguments at all.""" 302 result = self.runMkenums() 303 self.assertEqual('', result.err) 304 self.assertEqual('''/* {standard_top_comment} */ 305 306 307/* {standard_bottom_comment} */'''.format(**result.subs), 308 result.out.strip()) 309 310 def test_empty_template(self): 311 """Test running with an empty template and no header files.""" 312 result = self.runMkenumsWithTemplate('') 313 self.assertEqual('', result.err) 314 self.assertEqual('''/* {standard_top_comment} */ 315 316 317/* {standard_bottom_comment} */'''.format(**result.subs), 318 result.out.strip()) 319 320 def test_no_headers(self): 321 """Test running with a complete template, but no header files.""" 322 result = self.runMkenumsWithAllSubstitutions() 323 self.assertEqual('', result.err) 324 self.assertEqual(''' 325comment 326comment: {standard_top_comment} 327 328 329file-header 330file-tail 331 332comment 333comment: {standard_bottom_comment} 334'''.format(**result.subs).strip(), result.out) 335 336 def test_empty_header(self): 337 """Test an empty header.""" 338 result = self.runMkenumsWithHeader('') 339 self.assertEqual('', result.err) 340 self.assertEqual(''' 341comment 342comment: {standard_top_comment} 343 344 345file-header 346file-tail 347 348comment 349comment: {standard_bottom_comment} 350'''.format(**result.subs).strip(), result.out) 351 352 def test_enum_name(self): 353 """Test typedefs with an enum and a typedef name. Bug #794506.""" 354 h_contents = ''' 355 typedef enum _SomeEnumIdentifier { 356 ENUM_VALUE 357 } SomeEnumIdentifier; 358 ''' 359 result = self.runMkenumsWithHeader(h_contents) 360 self.assertEqual('', result.err) 361 self.assertSingleEnum(result, 'SomeEnumIdentifier', 362 'some_enum_identifier', 'SOME_ENUM_IDENTIFIER', 363 'ENUM_IDENTIFIER', 'SOME', 'enum', 'Enum', 364 'ENUM', 'ENUM_VALUE', 'value', '0') 365 366 def test_non_utf8_encoding(self): 367 """Test source files with non-UTF-8 encoding. Bug #785113.""" 368 h_contents = ''' 369 /* Copyright © La Peña */ 370 typedef enum { 371 ENUM_VALUE 372 } SomeEnumIdentifier; 373 ''' 374 result = self.runMkenumsWithHeader(h_contents, encoding='iso-8859-1') 375 self.assertIn('WARNING: UnicodeWarning: ', result.err) 376 self.assertSingleEnum(result, 'SomeEnumIdentifier', 377 'some_enum_identifier', 'SOME_ENUM_IDENTIFIER', 378 'ENUM_IDENTIFIER', 'SOME', 'enum', 'Enum', 379 'ENUM', 'ENUM_VALUE', 'value', '0') 380 381 def test_reproducible(self): 382 """Test builds are reproducible regardless of file ordering. 383 Bug #691436.""" 384 template_contents = 'template' 385 386 h_contents1 = ''' 387 typedef enum { 388 FIRST, 389 } Header1; 390 ''' 391 392 h_contents2 = ''' 393 typedef enum { 394 SECOND, 395 } Header2; 396 ''' 397 398 with tempfile.NamedTemporaryFile(dir=self.tmpdir.name, 399 suffix='1.h', delete=False) as h_file1, \ 400 tempfile.NamedTemporaryFile(dir=self.tmpdir.name, 401 suffix='2.h', delete=False) as h_file2: 402 # Write out the headers. 403 h_file1.write(h_contents1.encode('utf-8')) 404 h_file2.write(h_contents2.encode('utf-8')) 405 406 h_file1.flush() 407 h_file2.flush() 408 409 # Run glib-mkenums with the headers in one order, and then again 410 # in another order. 411 result1 = self.runMkenumsWithTemplate(template_contents, 412 h_file1.name, h_file2.name) 413 self.assertEqual('', result1.err) 414 415 result2 = self.runMkenumsWithTemplate(template_contents, 416 h_file2.name, h_file1.name) 417 self.assertEqual('', result2.err) 418 419 # The output should be the same. 420 self.assertEqual(result1.out, result2.out) 421 422 def test_no_nick(self): 423 """Test trigraphs with a desc but no nick. Issue #1360.""" 424 h_contents = ''' 425 typedef enum { 426 GEGL_SAMPLER_NEAREST = 0, /*< desc="nearest" >*/ 427 } GeglSamplerType; 428 ''' 429 result = self.runMkenumsWithHeader(h_contents) 430 self.assertEqual('', result.err) 431 self.assertSingleEnum(result, 'GeglSamplerType', 432 'gegl_sampler_type', 'GEGL_SAMPLER_TYPE', 433 'SAMPLER_TYPE', 'GEGL', 'enum', 'Enum', 434 'ENUM', 'GEGL_SAMPLER_NEAREST', 'nearest', '0') 435 436 def test_filename_basename_in_fhead_ftail(self): 437 template_contents = ''' 438/*** BEGIN file-header ***/ 439file-header 440filename: @filename@ 441basename: @basename@ 442/*** END file-header ***/ 443 444/*** BEGIN comment ***/ 445comment 446comment: @comment@ 447/*** END comment ***/ 448 449/*** BEGIN file-tail ***/ 450file-tail 451filename: @filename@ 452basename: @basename@ 453/*** END file-tail ***/''' 454 result = self.runMkenumsWithTemplate(template_contents) 455 self.assertEqual( 456 textwrap.dedent( 457 ''' 458 WARNING: @filename@ used in file-header section. 459 WARNING: @basename@ used in file-header section. 460 WARNING: @filename@ used in file-tail section. 461 WARNING: @basename@ used in file-tail section. 462 ''').strip(), 463 result.err) 464 self.assertEqual(''' 465comment 466comment: {standard_top_comment} 467 468 469file-header 470filename: @filename@ 471basename: @basename@ 472file-tail 473filename: @filename@ 474basename: @basename@ 475 476comment 477comment: {standard_bottom_comment} 478'''.format(**result.subs).strip(), result.out) 479 480 481class TestRspMkenums(TestMkenums): 482 '''Run all tests again in @rspfile mode''' 483 rspfile = True 484 485 486if __name__ == '__main__': 487 unittest.main(testRunner=taptestrunner.TAPTestRunner()) 488