1 #!/usr/bin/python
2 '''Usage: %s [OPTIONS] <input file(s)>
3 Generate test source file for CxxTest.
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 '''
23 import re
24 import sys
25 import getopt
26 import glob
27 import string
29 # Global variables
30 suites = []
31 suite = None
32 inBlock = 0
34 outputFileName = None
35 runner = None
36 gui = None
37 root = None
38 part = None
39 noStaticInit = None
40 templateFileName = None
41 headers = []
43 haveExceptionHandling = 0
44 noExceptionHandling = 0
45 haveStandardLibrary = 0
46 noStandardLibrary = 0
47 abortOnFail = 0
48 factor = 0
49 longlong = 0
51 def main():
52 '''The main program'''
53 files = parseCommandline()
54 scanInputFiles( files )
55 writeOutput()
57 def 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 )
66 def usageString():
67 '''Construct program usage string'''
68 return __doc__ % sys.argv[0]
70 def 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)
77 def 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, problem:
86 usage( problem )
87 setOptions( options )
88 return setFiles( patterns )
90 def 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'
136 if noStaticInit and (root or part):
137 abort( '--no-static-init cannot be used with --root/--part' )
139 if gui and not runner:
140 runner = 'StdioPrinter'
142 def printVersion():
143 '''Print CxxTest version and exit'''
144 sys.stdout.write( "This is CxxTest version 3.10.1.\n" )
145 sys.exit(0)
147 def 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
154 def 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
163 def fixBackslashes( fileName ):
164 '''Convert backslashes to slashes in file name'''
165 return re.sub( r'\\', '/', fileName, 0 )
167 def 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' )
175 def 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
185 scanInputLine( fileName, lineNo, line )
186 closeSuite()
187 file.close()
189 def scanInputLine( fileName, lineNo, line ):
190 '''Scan single input line for interesting stuff'''
191 scanLineForExceptionHandling( line )
192 scanLineForStandardLibrary( line )
194 scanLineForSuiteStart( fileName, lineNo, line )
196 global suite
197 if suite:
198 scanLineInsideSuite( suite, lineNo, line )
200 def 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 )
208 def 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
215 global inBlock
216 if not inBlock:
217 inBlock = lineStartsBlock( line )
218 if inBlock:
219 inBlock = addLineToBlock( suite, lineNo, line )
220 return inBlock
223 std_re = re.compile( r"\b(std\s*::|CXXTEST_STD|using\s+namespace\s+std\b|^\s*\#\s*include\s+<[a-z0-9]+>)" )
224 def 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
231 exception_re = re.compile( r"\b(throw|try|catch|TSM?_ASSERT_THROWS[A-Z_]*)\b" )
232 def 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
239 suite_re = re.compile( r'\bclass\s+(\w+)\s*:\s*public\s+((::)?\s*CxxTest\s*::\s*)?TestSuite\b' )
240 generatedSuite_re = re.compile( r'\bCXXTEST_SUITE\s*\(\s*(\w*)\s*\)' )
241 def 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 )
251 def 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' : [] }
266 def 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
270 test_re = re.compile( r'^([^/]|/[^/])*\bvoid\s+([Tt]est\w+)\s*\(\s*(void)?\s*\)' )
271 def 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 )
277 def 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 )
287 def 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 )
292 e = re.search( r'\}\}', line )
293 if e:
294 line = line[:e.start()]
295 suite['lines'].append( line )
296 return e is None
298 def 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 )
304 create_re = re.compile( r'\bstatic\s+\w+\s*\*\s*createSuite\s*\(\s*(void)?\s*\)' )
305 def scanLineForCreate( suite, lineNo, line ):
306 '''Check if current line defines a createSuite() function'''
307 if create_re.search( line ):
308 addSuiteCreateDestroy( suite, 'create', lineNo )
310 destroy_re = re.compile( r'\bstatic\s+void\s+destroySuite\s*\(\s*\w+\s*\*\s*\w*\s*\)' )
311 def scanLineForDestroy( suite, lineNo, line ):
312 '''Check if current line defines a destroySuite() function'''
313 if destroy_re.search( line ):
314 addSuiteCreateDestroy( suite, 'destroy', lineNo )
316 def cstr( str ):
317 '''Convert a string to its C representation'''
318 return '"' + string.replace( str, '\\', '\\\\' ) + '"'
321 def 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
327 def 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
336 def verifySuite(suite):
337 '''Verify current suite is legal'''
338 if suite.has_key('create') and not suite.has_key('destroy'):
339 abort( '%s:%s: Suite %s has createSuite() but no destroySuite()' %
340 (suite['file'], suite['create'], suite['name']) )
341 if suite.has_key('destroy') and not suite.has_key('create'):
342 abort( '%s:%s: Suite %s has destroySuite() but no createSuite()' %
343 (suite['file'], suite['destroy'], suite['name']) )
345 def rememberSuite(suite):
346 '''Add current suite to list'''
347 global suites
348 suites.append( suite )
350 def writeOutput():
351 '''Create output file'''
352 if templateFileName:
353 writeTemplateOutput()
354 else:
355 writeSimpleOutput()
357 def writeSimpleOutput():
358 '''Create output not based on template'''
359 output = startOutputFile()
360 writePreamble( output )
361 writeMain( output )
362 writeWorld( output )
363 output.close()
365 include_re = re.compile( r"\s*\#\s*include\s+<cxxtest/" )
366 preamble_re = re.compile( r"^\s*<CxxTest\s+preamble>\s*$" )
367 world_re = re.compile( r"^\s*<CxxTest\s+world>\s*$" )
368 def 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()
388 def 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
397 wrotePreamble = 0
398 def 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
429 def 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' )
444 wroteWorld = 0
445 def 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
457 def 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 )
471 def isGenerated(suite):
472 '''Checks whether a suite class should be created'''
473 return suite['generated']
475 def isDynamic(suite):
476 '''Checks whether a suite is dynamic'''
477 return suite.has_key('create')
479 lastIncluded = ''
480 def 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
487 def 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' )
495 def 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']) )
502 def writeSuiteObject( output, suite ):
503 '''Create static suite object for non-dynamic suites'''
504 output.writelines( [ "static ", suite['name'], " ", suite['object'], ";\n\n" ] )
506 def 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'] )
513 def writeTestDescriptions( output, suite ):
514 '''Write all test descriptions for a suite'''
515 for test in suite['tests']:
516 writeTestDescription( output, suite, test )
518 def 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'] )
528 def runBody( suite, test ):
529 '''Body of TestDescription::run()'''
530 if isDynamic(suite): return dynamicRun( suite, test )
531 else: return staticRun( suite, test )
533 def dynamicRun( suite, test ):
534 '''Body of TestDescription::run() for test in a dynamic suite'''
535 return 'if ( ' + suite['object'] + ' ) ' + suite['object'] + '->' + test['name'] + '();'
537 def staticRun( suite, test ):
538 '''Body of TestDescription::run() for test in a non-dynamic suite'''
539 return suite['object'] + '.' + test['name'] + '();'
541 def writeSuiteDescription( output, suite ):
542 '''Write SuiteDescription object'''
543 if isDynamic( suite ):
544 writeDynamicDescription( output, suite )
545 else:
546 writeStaticDescription( output, suite )
548 def 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' )
557 def 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' )
565 def writeRoot(output):
566 '''Write static members of CxxTest classes'''
567 output.write( '#include <cxxtest/Root.cpp>\n' )
569 def 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']) )
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']) )
590 output.write( ' }\n' )
591 output.write( '}\n' )
593 main()