1#!/usr/bin/env python 2'''Usage: %s [OPTIONS] <input file(s)> 3Generate test source file for CxxTest. 4 5 -v, --version Write CxxTest version 6 -o, --output=NAME Write output to file NAME 7 --runner=CLASS Create a main() function that runs CxxTest::CLASS 8 --gui=CLASS Like --runner, with GUI component 9 --error-printer Same as --runner=ErrorPrinter 10 --abort-on-fail Abort tests on failed asserts (like xUnit) 11 --have-std Use standard library (even if not found in tests) 12 --no-std Don\'t use standard library (even if found in tests) 13 --have-eh Use exception handling (even if not found in tests) 14 --no-eh Don\'t use exception handling (even if found in tests) 15 --longlong=[TYPE] Use TYPE (default: long long) as long long 16 --template=TEMPLATE Use TEMPLATE file to generate the test runner 17 --include=HEADER Include HEADER in test runner before other headers 18 --root Write CxxTest globals 19 --part Don\'t write CxxTest globals 20 --no-static-init Don\'t rely on static initialization 21''' 22 23import re 24import sys 25import getopt 26import glob 27import string 28 29# Global variables 30suites = [] 31suite = None 32inBlock = 0 33 34outputFileName = None 35runner = None 36gui = None 37root = None 38part = None 39noStaticInit = None 40templateFileName = None 41headers = [] 42 43haveExceptionHandling = 0 44noExceptionHandling = 0 45haveStandardLibrary = 0 46noStandardLibrary = 0 47abortOnFail = 0 48factor = 0 49longlong = 0 50 51def main(): 52 '''The main program''' 53 files = parseCommandline() 54 scanInputFiles( files ) 55 writeOutput() 56 57def usage( problem = None ): 58 '''Print usage info and exit''' 59 if problem is None: 60 print( usageString() ) 61 sys.exit(0) 62 else: 63 sys.stderr.write( usageString() ) 64 abort( problem ) 65 66def usageString(): 67 '''Construct program usage string''' 68 return __doc__ % sys.argv[0] 69 70def abort( problem ): 71 '''Print error message and exit''' 72 sys.stderr.write( '\n' ) 73 sys.stderr.write( problem ) 74 sys.stderr.write( '\n\n' ) 75 sys.exit(2) 76 77def parseCommandline(): 78 '''Analyze command line arguments''' 79 try: 80 options, patterns = getopt.getopt( sys.argv[1:], 'o:r:', 81 ['version', 'output=', 'runner=', 'gui=', 82 'error-printer', 'abort-on-fail', 'have-std', 'no-std', 83 'have-eh', 'no-eh', 'template=', 'include=', 84 'root', 'part', 'no-static-init', 'factor', 'longlong='] ) 85 except getopt.error as problem: 86 usage( problem ) 87 setOptions( options ) 88 return setFiles( patterns ) 89 90def setOptions( options ): 91 '''Set options specified on command line''' 92 global outputFileName, templateFileName, runner, gui, haveStandardLibrary, factor, longlong 93 global haveExceptionHandling, noExceptionHandling, abortOnFail, headers, root, part, noStaticInit 94 for o, a in options: 95 if o in ('-v', '--version'): 96 printVersion() 97 elif o in ('-o', '--output'): 98 outputFileName = a 99 elif o == '--template': 100 templateFileName = a 101 elif o == '--runner': 102 runner = a 103 elif o == '--gui': 104 gui = a 105 elif o == '--include': 106 if not re.match( r'^["<].*[>"]$', a ): 107 a = ('"%s"' % a) 108 headers.append( a ) 109 elif o == '--error-printer': 110 runner = 'ErrorPrinter' 111 haveStandardLibrary = 1 112 elif o == '--abort-on-fail': 113 abortOnFail = 1 114 elif o == '--have-std': 115 haveStandardLibrary = 1 116 elif o == '--no-std': 117 noStandardLibrary = 1 118 elif o == '--have-eh': 119 haveExceptionHandling = 1 120 elif o == '--no-eh': 121 noExceptionHandling = 1 122 elif o == '--root': 123 root = 1 124 elif o == '--part': 125 part = 1 126 elif o == '--no-static-init': 127 noStaticInit = 1 128 elif o == '--factor': 129 factor = 1 130 elif o == '--longlong': 131 if a: 132 longlong = a 133 else: 134 longlong = 'long long' 135 136 if noStaticInit and (root or part): 137 abort( '--no-static-init cannot be used with --root/--part' ) 138 139 if gui and not runner: 140 runner = 'StdioPrinter' 141 142def printVersion(): 143 '''Print CxxTest version and exit''' 144 sys.stdout.write( "This is CxxTest version 3.10.1.\n" ) 145 sys.exit(0) 146 147def setFiles( patterns ): 148 '''Set input files specified on command line''' 149 files = expandWildcards( patterns ) 150 if len(files) is 0 and not root: 151 usage( "No input files found" ) 152 return files 153 154def expandWildcards( patterns ): 155 '''Expand all wildcards in an array (glob)''' 156 fileNames = [] 157 for pathName in patterns: 158 patternFiles = glob.glob( pathName ) 159 for fileName in patternFiles: 160 fileNames.append( fixBackslashes( fileName ) ) 161 return fileNames 162 163def fixBackslashes( fileName ): 164 '''Convert backslashes to slashes in file name''' 165 return re.sub( r'\\', '/', fileName, 0 ) 166 167def scanInputFiles(files): 168 '''Scan all input files for test suites''' 169 for file in files: 170 scanInputFile(file) 171 global suites 172 if len(suites) is 0 and not root: 173 abort( 'No tests defined' ) 174 175def scanInputFile(fileName): 176 '''Scan single input file for test suites''' 177 file = open(fileName) 178 lineNo = 0 179 while 1: 180 line = file.readline() 181 if not line: 182 break 183 lineNo = lineNo + 1 184 185 scanInputLine( fileName, lineNo, line ) 186 closeSuite() 187 file.close() 188 189def scanInputLine( fileName, lineNo, line ): 190 '''Scan single input line for interesting stuff''' 191 scanLineForExceptionHandling( line ) 192 scanLineForStandardLibrary( line ) 193 194 scanLineForSuiteStart( fileName, lineNo, line ) 195 196 global suite 197 if suite: 198 scanLineInsideSuite( suite, lineNo, line ) 199 200def scanLineInsideSuite( suite, lineNo, line ): 201 '''Analyze line which is part of a suite''' 202 global inBlock 203 if lineBelongsToSuite( suite, lineNo, line ): 204 scanLineForTest( suite, lineNo, line ) 205 scanLineForCreate( suite, lineNo, line ) 206 scanLineForDestroy( suite, lineNo, line ) 207 208def lineBelongsToSuite( suite, lineNo, line ): 209 '''Returns whether current line is part of the current suite. 210 This can be false when we are in a generated suite outside of CXXTEST_CODE() blocks 211 If the suite is generated, adds the line to the list of lines''' 212 if not suite['generated']: 213 return 1 214 215 global inBlock 216 if not inBlock: 217 inBlock = lineStartsBlock( line ) 218 if inBlock: 219 inBlock = addLineToBlock( suite, lineNo, line ) 220 return inBlock 221 222 223std_re = re.compile( r"\b(std\s*::|CXXTEST_STD|using\s+namespace\s+std\b|^\s*\#\s*include\s+<[a-z0-9]+>)" ) 224def scanLineForStandardLibrary( line ): 225 '''Check if current line uses standard library''' 226 global haveStandardLibrary, noStandardLibrary 227 if not haveStandardLibrary and std_re.search(line): 228 if not noStandardLibrary: 229 haveStandardLibrary = 1 230 231exception_re = re.compile( r"\b(throw|try|catch|TSM?_ASSERT_THROWS[A-Z_]*)\b" ) 232def scanLineForExceptionHandling( line ): 233 '''Check if current line uses exception handling''' 234 global haveExceptionHandling, noExceptionHandling 235 if not haveExceptionHandling and exception_re.search(line): 236 if not noExceptionHandling: 237 haveExceptionHandling = 1 238 239suite_re = re.compile( r'\bclass\s+(\w+)\s*:\s*public\s+((::)?\s*CxxTest\s*::\s*)?TestSuite\b' ) 240generatedSuite_re = re.compile( r'\bCXXTEST_SUITE\s*\(\s*(\w*)\s*\)' ) 241def scanLineForSuiteStart( fileName, lineNo, line ): 242 '''Check if current line starts a new test suite''' 243 m = suite_re.search( line ) 244 if m: 245 startSuite( m.group(1), fileName, lineNo, 0 ) 246 m = generatedSuite_re.search( line ) 247 if m: 248 sys.stdout.write( "%s:%s: Warning: Inline test suites are deprecated.\n" % (fileName, lineNo) ) 249 startSuite( m.group(1), fileName, lineNo, 1 ) 250 251def startSuite( name, file, line, generated ): 252 '''Start scanning a new suite''' 253 global suite 254 closeSuite() 255 suite = { 'name' : name, 256 'file' : file, 257 'cfile' : cstr(file), 258 'line' : line, 259 'generated' : generated, 260 'object' : 'suite_%s' % name, 261 'dobject' : 'suiteDescription_%s' % name, 262 'tlist' : 'Tests_%s' % name, 263 'tests' : [], 264 'lines' : [] } 265 266def lineStartsBlock( line ): 267 '''Check if current line starts a new CXXTEST_CODE() block''' 268 return re.search( r'\bCXXTEST_CODE\s*\(', line ) is not None 269 270test_re = re.compile( r'^([^/]|/[^/])*\bvoid\s+([Tt]est\w+)\s*\(\s*(void)?\s*\)' ) 271def scanLineForTest( suite, lineNo, line ): 272 '''Check if current line starts a test''' 273 m = test_re.search( line ) 274 if m: 275 addTest( suite, m.group(2), lineNo ) 276 277def addTest( suite, name, line ): 278 '''Add a test function to the current suite''' 279 test = { 'name' : name, 280 'suite' : suite, 281 'class' : 'TestDescription_%s_%s' % (suite['name'], name), 282 'object' : 'testDescription_%s_%s' % (suite['name'], name), 283 'line' : line, 284 } 285 suite['tests'].append( test ) 286 287def addLineToBlock( suite, lineNo, line ): 288 '''Append the line to the current CXXTEST_CODE() block''' 289 line = fixBlockLine( suite, lineNo, line ) 290 line = re.sub( r'^.*\{\{', '', line ) 291 292 e = re.search( r'\}\}', line ) 293 if e: 294 line = line[:e.start()] 295 suite['lines'].append( line ) 296 return e is None 297 298def fixBlockLine( suite, lineNo, line): 299 '''Change all [E]TS_ macros used in a line to _[E]TS_ macros with the correct file/line''' 300 return re.sub( r'\b(E?TSM?_(ASSERT[A-Z_]*|FAIL))\s*\(', 301 r'_\1(%s,%s,' % (suite['cfile'], lineNo), 302 line, 0 ) 303 304create_re = re.compile( r'\bstatic\s+\w+\s*\*\s*createSuite\s*\(\s*(void)?\s*\)' ) 305def scanLineForCreate( suite, lineNo, line ): 306 '''Check if current line defines a createSuite() function''' 307 if create_re.search( line ): 308 addSuiteCreateDestroy( suite, 'create', lineNo ) 309 310destroy_re = re.compile( r'\bstatic\s+void\s+destroySuite\s*\(\s*\w+\s*\*\s*\w*\s*\)' ) 311def scanLineForDestroy( suite, lineNo, line ): 312 '''Check if current line defines a destroySuite() function''' 313 if destroy_re.search( line ): 314 addSuiteCreateDestroy( suite, 'destroy', lineNo ) 315 316def cstr( str ): 317 '''Convert a string to its C representation''' 318 return '"' + str.replace( '\\', '\\\\' ) + '"' 319 320 321def addSuiteCreateDestroy( suite, which, line ): 322 '''Add createSuite()/destroySuite() to current suite''' 323 if suite.has_key(which): 324 abort( '%s:%s: %sSuite() already declared' % ( suite['file'], str(line), which ) ) 325 suite[which] = line 326 327def closeSuite(): 328 '''Close current suite and add it to the list if valid''' 329 global suite 330 if suite is not None: 331 if len(suite['tests']) is not 0: 332 verifySuite(suite) 333 rememberSuite(suite) 334 suite = None 335 336def verifySuite(suite): 337 '''Verify current suite is legal''' 338 if 'create' in suite and not 'destroy' in suite: 339 abort( '%s:%s: Suite %s has createSuite() but no destroySuite()' % 340 (suite['file'], suite['create'], suite['name']) ) 341 if 'destroy' in suite and not 'create' in suite: 342 abort( '%s:%s: Suite %s has destroySuite() but no createSuite()' % 343 (suite['file'], suite['destroy'], suite['name']) ) 344 345def rememberSuite(suite): 346 '''Add current suite to list''' 347 global suites 348 suites.append( suite ) 349 350def writeOutput(): 351 '''Create output file''' 352 if templateFileName: 353 writeTemplateOutput() 354 else: 355 writeSimpleOutput() 356 357def writeSimpleOutput(): 358 '''Create output not based on template''' 359 output = startOutputFile() 360 writePreamble( output ) 361 writeMain( output ) 362 writeWorld( output ) 363 output.close() 364 365include_re = re.compile( r"\s*\#\s*include\s+<cxxtest/" ) 366preamble_re = re.compile( r"^\s*<CxxTest\s+preamble>\s*$" ) 367world_re = re.compile( r"^\s*<CxxTest\s+world>\s*$" ) 368def writeTemplateOutput(): 369 '''Create output based on template file''' 370 template = open(templateFileName) 371 output = startOutputFile() 372 while 1: 373 line = template.readline() 374 if not line: 375 break; 376 if include_re.search( line ): 377 writePreamble( output ) 378 output.write( line ) 379 elif preamble_re.search( line ): 380 writePreamble( output ) 381 elif world_re.search( line ): 382 writeWorld( output ) 383 else: 384 output.write( line ) 385 template.close() 386 output.close() 387 388def startOutputFile(): 389 '''Create output file and write header''' 390 if outputFileName is not None: 391 output = open( outputFileName, 'w' ) 392 else: 393 output = sys.stdout 394 output.write( "/* Generated file, do not edit */\n\n" ) 395 return output 396 397wrotePreamble = 0 398def writePreamble( output ): 399 '''Write the CxxTest header (#includes and #defines)''' 400 global wrotePreamble, headers, longlong 401 if wrotePreamble: return 402 output.write( "#ifndef CXXTEST_RUNNING\n" ) 403 output.write( "#define CXXTEST_RUNNING\n" ) 404 output.write( "#endif\n" ) 405 output.write( "\n" ) 406 if haveStandardLibrary: 407 output.write( "#define _CXXTEST_HAVE_STD\n" ) 408 if haveExceptionHandling: 409 output.write( "#define _CXXTEST_HAVE_EH\n" ) 410 if abortOnFail: 411 output.write( "#define _CXXTEST_ABORT_TEST_ON_FAIL\n" ) 412 if longlong: 413 output.write( "#define _CXXTEST_LONGLONG %s\n" % longlong ) 414 if factor: 415 output.write( "#define _CXXTEST_FACTOR\n" ) 416 for header in headers: 417 output.write( "#include %s\n" % header ) 418 output.write( "#include <cxxtest/TestListener.h>\n" ) 419 output.write( "#include <cxxtest/TestTracker.h>\n" ) 420 output.write( "#include <cxxtest/TestRunner.h>\n" ) 421 output.write( "#include <cxxtest/RealDescriptions.h>\n" ) 422 if runner: 423 output.write( "#include <cxxtest/%s.h>\n" % runner ) 424 if gui: 425 output.write( "#include <cxxtest/%s.h>\n" % gui ) 426 output.write( "\n" ) 427 wrotePreamble = 1 428 429def writeMain( output ): 430 '''Write the main() function for the test runner''' 431 if gui: 432 output.write( 'int main( int argc, char *argv[] ) {\n' ) 433 if noStaticInit: 434 output.write( ' CxxTest::initialize();\n' ) 435 output.write( ' return CxxTest::GuiTuiRunner<CxxTest::%s, CxxTest::%s>( argc, argv ).run();\n' % (gui, runner) ) 436 output.write( '}\n' ) 437 elif runner: 438 output.write( 'int main() {\n' ) 439 if noStaticInit: 440 output.write( ' CxxTest::initialize();\n' ) 441 output.write( ' return CxxTest::%s().run();\n' % runner ) 442 output.write( '}\n' ) 443 444wroteWorld = 0 445def writeWorld( output ): 446 '''Write the world definitions''' 447 global wroteWorld, part 448 if wroteWorld: return 449 writePreamble( output ) 450 writeSuites( output ) 451 if root or not part: 452 writeRoot( output ) 453 if noStaticInit: 454 writeInitialize( output ) 455 wroteWorld = 1 456 457def writeSuites(output): 458 '''Write all TestDescriptions and SuiteDescriptions''' 459 for suite in suites: 460 writeInclude( output, suite['file'] ) 461 if isGenerated(suite): 462 generateSuite( output, suite ) 463 if isDynamic(suite): 464 writeSuitePointer( output, suite ) 465 else: 466 writeSuiteObject( output, suite ) 467 writeTestList( output, suite ) 468 writeSuiteDescription( output, suite ) 469 writeTestDescriptions( output, suite ) 470 471def isGenerated(suite): 472 '''Checks whether a suite class should be created''' 473 return suite['generated'] 474 475def isDynamic(suite): 476 '''Checks whether a suite is dynamic''' 477 return 'create' in suite 478 479lastIncluded = '' 480def writeInclude(output, file): 481 '''Add #include "file" statement''' 482 global lastIncluded 483 if file == lastIncluded: return 484 output.writelines( [ '#include "', file, '"\n\n' ] ) 485 lastIncluded = file 486 487def generateSuite( output, suite ): 488 '''Write a suite declared with CXXTEST_SUITE()''' 489 output.write( 'class %s : public CxxTest::TestSuite {\n' % suite['name'] ) 490 output.write( 'public:\n' ) 491 for line in suite['lines']: 492 output.write(line) 493 output.write( '};\n\n' ) 494 495def writeSuitePointer( output, suite ): 496 '''Create static suite pointer object for dynamic suites''' 497 if noStaticInit: 498 output.write( 'static %s *%s;\n\n' % (suite['name'], suite['object']) ) 499 else: 500 output.write( 'static %s *%s = 0;\n\n' % (suite['name'], suite['object']) ) 501 502def writeSuiteObject( output, suite ): 503 '''Create static suite object for non-dynamic suites''' 504 output.writelines( [ "static ", suite['name'], " ", suite['object'], ";\n\n" ] ) 505 506def writeTestList( output, suite ): 507 '''Write the head of the test linked list for a suite''' 508 if noStaticInit: 509 output.write( 'static CxxTest::List %s;\n' % suite['tlist'] ) 510 else: 511 output.write( 'static CxxTest::List %s = { 0, 0 };\n' % suite['tlist'] ) 512 513def writeTestDescriptions( output, suite ): 514 '''Write all test descriptions for a suite''' 515 for test in suite['tests']: 516 writeTestDescription( output, suite, test ) 517 518def writeTestDescription( output, suite, test ): 519 '''Write test description object''' 520 output.write( 'static class %s : public CxxTest::RealTestDescription {\n' % test['class'] ) 521 output.write( 'public:\n' ) 522 if not noStaticInit: 523 output.write( ' %s() : CxxTest::RealTestDescription( %s, %s, %s, "%s" ) {}\n' % 524 (test['class'], suite['tlist'], suite['dobject'], test['line'], test['name']) ) 525 output.write( ' void runTest() { %s }\n' % runBody( suite, test ) ) 526 output.write( '} %s;\n\n' % test['object'] ) 527 528def runBody( suite, test ): 529 '''Body of TestDescription::run()''' 530 if isDynamic(suite): return dynamicRun( suite, test ) 531 else: return staticRun( suite, test ) 532 533def dynamicRun( suite, test ): 534 '''Body of TestDescription::run() for test in a dynamic suite''' 535 return 'if ( ' + suite['object'] + ' ) ' + suite['object'] + '->' + test['name'] + '();' 536 537def staticRun( suite, test ): 538 '''Body of TestDescription::run() for test in a non-dynamic suite''' 539 return suite['object'] + '.' + test['name'] + '();' 540 541def writeSuiteDescription( output, suite ): 542 '''Write SuiteDescription object''' 543 if isDynamic( suite ): 544 writeDynamicDescription( output, suite ) 545 else: 546 writeStaticDescription( output, suite ) 547 548def writeDynamicDescription( output, suite ): 549 '''Write SuiteDescription for a dynamic suite''' 550 output.write( 'CxxTest::DynamicSuiteDescription<%s> %s' % (suite['name'], suite['dobject']) ) 551 if not noStaticInit: 552 output.write( '( %s, %s, "%s", %s, %s, %s, %s )' % 553 (suite['cfile'], suite['line'], suite['name'], suite['tlist'], 554 suite['object'], suite['create'], suite['destroy']) ) 555 output.write( ';\n\n' ) 556 557def writeStaticDescription( output, suite ): 558 '''Write SuiteDescription for a static suite''' 559 output.write( 'CxxTest::StaticSuiteDescription %s' % suite['dobject'] ) 560 if not noStaticInit: 561 output.write( '( %s, %s, "%s", %s, %s )' % 562 (suite['cfile'], suite['line'], suite['name'], suite['object'], suite['tlist']) ) 563 output.write( ';\n\n' ) 564 565def writeRoot(output): 566 '''Write static members of CxxTest classes''' 567 output.write( '#include <cxxtest/Root.cpp>\n' ) 568 569def writeInitialize(output): 570 '''Write CxxTest::initialize(), which replaces static initialization''' 571 output.write( 'namespace CxxTest {\n' ) 572 output.write( ' void initialize()\n' ) 573 output.write( ' {\n' ) 574 for suite in suites: 575 output.write( ' %s.initialize();\n' % suite['tlist'] ) 576 if isDynamic(suite): 577 output.write( ' %s = 0;\n' % suite['object'] ) 578 output.write( ' %s.initialize( %s, %s, "%s", %s, %s, %s, %s );\n' % 579 (suite['dobject'], suite['cfile'], suite['line'], suite['name'], 580 suite['tlist'], suite['object'], suite['create'], suite['destroy']) ) 581 else: 582 output.write( ' %s.initialize( %s, %s, "%s", %s, %s );\n' % 583 (suite['dobject'], suite['cfile'], suite['line'], suite['name'], 584 suite['object'], suite['tlist']) ) 585 586 for test in suite['tests']: 587 output.write( ' %s.initialize( %s, %s, %s, "%s" );\n' % 588 (test['object'], suite['tlist'], suite['dobject'], test['line'], test['name']) ) 589 590 output.write( ' }\n' ) 591 output.write( '}\n' ) 592 593main() 594