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