1#-------------------------------------------------------------------------
2# CxxTest: A lightweight C++ unit testing library.
3# Copyright (c) 2008 Sandia Corporation.
4# This software is distributed under the LGPL License v2.1
5# For more information, see the COPYING file in the top CxxTest directory.
6# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
7# the U.S. Government retains certain rights in this software.
8#-------------------------------------------------------------------------
9
10# vim: fileencoding=utf-8
11
12from __future__ import division
13# the above import important for forward-compatibility with python3,
14# which is already the default in archlinux!
15
16__all__ = ['main', 'create_manpage']
17
18import __release__
19import os
20import sys
21import re
22import glob
23from optparse import OptionParser
24import cxxtest_parser
25from string import Template
26
27try:
28    import cxxtest_fog
29    imported_fog=True
30except ImportError:
31    imported_fog=False
32
33from cxxtest_misc import abort
34
35options = []
36suites = []
37
38wrotePreamble = 0
39wroteWorld = 0
40lastIncluded = ''
41
42def main(args=sys.argv):
43    '''The main program'''
44    #
45    # Reset global state
46    #
47    global wrotePreamble
48    wrotePreamble=0
49    global wroteWorld
50    wroteWorld=0
51    global lastIncluded
52    lastIncluded = ''
53
54    global suites
55    global options
56    files = parseCommandline(args)
57    if imported_fog and options.fog:
58        [options,suites] = cxxtest_fog.scanInputFiles( files, options )
59    else:
60        [options,suites] = cxxtest_parser.scanInputFiles( files, options )
61    writeOutput()
62
63def create_parser(asciidoc=False):
64    parser = OptionParser("cxxtestgen [options] [<filename> ...]")
65    if asciidoc:
66        parser.description="The cxxtestgen command processes C++ header files to perform test discovery, and then it creates files for the CxxTest test runner."
67    else:
68        parser.description="The 'cxxtestgen' command processes C++ header files to perform test discovery, and then it creates files for the 'CxxTest' test runner."
69
70    parser.add_option("--version",
71                      action="store_true", dest="version", default=False,
72                      help="Write the CxxTest version.")
73    parser.add_option("-o", "--output",
74                      dest="outputFileName", default=None, metavar="NAME",
75                      help="Write output to file NAME.")
76    parser.add_option("-w","--world", dest="world", default="cxxtest",
77                      help="The label of the tests, used to name the XML results.")
78    parser.add_option("", "--include", action="append",
79                      dest="headers", default=[], metavar="HEADER",
80                      help="Include file HEADER in the test runner before other headers.")
81    parser.add_option("", "--abort-on-fail",
82                      action="store_true", dest="abortOnFail", default=False,
83                      help="Abort tests on failed asserts (like xUnit).")
84    parser.add_option("", "--main",
85                      action="store", dest="main", default="main",
86                      help="Specify an alternative name for the main() function.")
87    parser.add_option("", "--headers",
88                      action="store", dest="header_filename", default=None,
89                      help="Specify a filename that contains a list of header files that are processed to generate a test runner.")
90    parser.add_option("", "--runner",
91                      dest="runner", default="", metavar="CLASS",
92                      help="Create a test runner that processes test events using the class CxxTest::CLASS.")
93    parser.add_option("", "--gui",
94                      dest="gui", metavar="CLASS",
95                      help="Create a GUI test runner that processes test events using the class CxxTest::CLASS. (deprecated)")
96    parser.add_option("", "--error-printer",
97                      action="store_true", dest="error_printer", default=False,
98                      help="Create a test runner using the ErrorPrinter class, and allow the use of the standard library.")
99    parser.add_option("", "--xunit-printer",
100                      action="store_true", dest="xunit_printer", default=False,
101                      help="Create a test runner using the XUnitPrinter class.")
102    parser.add_option("", "--xunit-file",  dest="xunit_file", default="",
103                      help="The file to which the XML summary is written for test runners using the XUnitPrinter class.  The default XML filename is TEST-<world>.xml, where <world> is the value of the --world option.  (default: cxxtest)")
104    parser.add_option("", "--have-std",
105                      action="store_true", dest="haveStandardLibrary", default=False,
106                      help="Use the standard library (even if not found in tests).")
107    parser.add_option("", "--no-std",
108                      action="store_true", dest="noStandardLibrary", default=False,
109                      help="Do not use standard library (even if found in tests).")
110    parser.add_option("", "--have-eh",
111                      action="store_true", dest="haveExceptionHandling", default=False,
112                      help="Use exception handling (even if not found in tests).")
113    parser.add_option("", "--no-eh",
114                      action="store_true", dest="noExceptionHandling", default=False,
115                      help="Do not use exception handling (even if found in tests).")
116    parser.add_option("", "--longlong",
117                      dest="longlong", default=None, metavar="TYPE",
118                      help="Use TYPE as for long long integers.  (default: not supported)")
119    parser.add_option("", "--no-static-init",
120                      action="store_true", dest="noStaticInit", default=False,
121                      help="Do not rely on static initialization in the test runner.")
122    parser.add_option("", "--template",
123                      dest="templateFileName", default=None, metavar="TEMPLATE",
124                      help="Generate the test runner using file TEMPLATE to define a template.")
125    parser.add_option("", "--root",
126                      action="store_true", dest="root", default=False,
127                      help="Write the main() function and global data for a test runner.")
128    parser.add_option("", "--part",
129                      action="store_true", dest="part", default=False,
130                      help="Write the tester classes for a test runner.")
131    #parser.add_option("", "--factor",
132                      #action="store_true", dest="factor", default=False,
133                      #help="Declare the _CXXTEST_FACTOR macro.  (deprecated)")
134    if imported_fog:
135        fog_help = "Use new FOG C++ parser"
136    else:
137        fog_help = "Use new FOG C++ parser (disabled)"
138    parser.add_option("-f", "--fog-parser",
139                        action="store_true",
140                        dest="fog",
141                        default=False,
142                        help=fog_help
143                        )
144    return parser
145
146def parseCommandline(args):
147    '''Analyze command line arguments'''
148    global imported_fog
149    global options
150
151    parser = create_parser()
152    (options, args) = parser.parse_args(args=args)
153    if not options.header_filename is None:
154        if not os.path.exists(options.header_filename):
155            abort( "ERROR: the file '%s' does not exist!" % options.header_filename )
156        INPUT = open(options.header_filename)
157        headers = [line.strip() for line in INPUT]
158        args.extend( headers )
159        INPUT.close()
160
161    if options.fog and not imported_fog:
162        abort( "Cannot use the FOG parser.  Check that the 'ply' package is installed.  The 'ordereddict' package is also required if running Python 2.6")
163
164    if options.version:
165      printVersion()
166
167    # the cxxtest builder relies on this behaviour! don't remove
168    if options.runner == 'none':
169        options.runner = None
170
171    if options.xunit_printer or options.runner == "XUnitPrinter":
172        options.xunit_printer=True
173        options.runner="XUnitPrinter"
174        if len(args) > 1:
175            if options.xunit_file == "":
176                if options.world == "":
177                    options.world = "cxxtest"
178                options.xunit_file="TEST-"+options.world+".xml"
179        elif options.xunit_file == "":
180            if options.world == "":
181                options.world = "cxxtest"
182            options.xunit_file="TEST-"+options.world+".xml"
183
184    if options.error_printer:
185      options.runner= "ErrorPrinter"
186      options.haveStandardLibrary = True
187
188    if options.noStaticInit and (options.root or options.part):
189        abort( '--no-static-init cannot be used with --root/--part' )
190
191    if options.gui and not options.runner:
192        options.runner = 'StdioPrinter'
193
194    files = setFiles(args[1:])
195    if len(files) == 0 and not options.root:
196        sys.stderr.write(parser.error("No input files found"))
197
198    return files
199
200
201def printVersion():
202    '''Print CxxTest version and exit'''
203    sys.stdout.write( "This is CxxTest version %s.\n" % __release__.__version__ )
204    sys.exit(0)
205
206def setFiles(patterns ):
207    '''Set input files specified on command line'''
208    files = expandWildcards( patterns )
209    return files
210
211def expandWildcards( patterns ):
212    '''Expand all wildcards in an array (glob)'''
213    fileNames = []
214    for pathName in patterns:
215        patternFiles = glob.glob( pathName )
216        for fileName in patternFiles:
217            fileNames.append( fixBackslashes( fileName ) )
218    return fileNames
219
220def fixBackslashes( fileName ):
221    '''Convert backslashes to slashes in file name'''
222    return re.sub( r'\\', '/', fileName, 0 )
223
224
225def writeOutput():
226    '''Create output file'''
227    if options.templateFileName:
228        writeTemplateOutput()
229    else:
230        writeSimpleOutput()
231
232def writeSimpleOutput():
233    '''Create output not based on template'''
234    output = startOutputFile()
235    writePreamble( output )
236    if options.root or not options.part:
237        writeMain( output )
238
239    if len(suites) > 0:
240        output.write("bool "+suites[0]['object']+"_init = false;\n")
241
242    writeWorld( output )
243    output.close()
244
245include_re = re.compile( r"\s*\#\s*include\s+<cxxtest/" )
246preamble_re = re.compile( r"^\s*<CxxTest\s+preamble>\s*$" )
247world_re = re.compile( r"^\s*<CxxTest\s+world>\s*$" )
248def writeTemplateOutput():
249    '''Create output based on template file'''
250    template = open(options.templateFileName)
251    output = startOutputFile()
252    while 1:
253        line = template.readline()
254        if not line:
255            break;
256        if include_re.search( line ):
257            writePreamble( output )
258            output.write( line )
259        elif preamble_re.search( line ):
260            writePreamble( output )
261        elif world_re.search( line ):
262            if len(suites) > 0:
263                output.write("bool "+suites[0]['object']+"_init = false;\n")
264            writeWorld( output )
265        else:
266            output.write( line )
267    template.close()
268    output.close()
269
270def startOutputFile():
271    '''Create output file and write header'''
272    if options.outputFileName is not None:
273        output = open( options.outputFileName, 'w' )
274    else:
275        output = sys.stdout
276    output.write( "/* Generated file, do not edit */\n\n" )
277    return output
278
279def writePreamble( output ):
280    '''Write the CxxTest header (#includes and #defines)'''
281    global wrotePreamble
282    if wrotePreamble: return
283    output.write( "#ifndef CXXTEST_RUNNING\n" )
284    output.write( "#define CXXTEST_RUNNING\n" )
285    output.write( "#endif\n" )
286    output.write( "\n" )
287    if options.xunit_printer:
288        output.write( "#include <fstream>\n" )
289    if options.haveStandardLibrary:
290        output.write( "#define _CXXTEST_HAVE_STD\n" )
291    if options.haveExceptionHandling:
292        output.write( "#define _CXXTEST_HAVE_EH\n" )
293    if options.abortOnFail:
294        output.write( "#define _CXXTEST_ABORT_TEST_ON_FAIL\n" )
295    if options.longlong:
296        output.write( "#define _CXXTEST_LONGLONG %s\n" % options.longlong )
297    #if options.factor:
298        #output.write( "#define _CXXTEST_FACTOR\n" )
299    for header in options.headers:
300        output.write( "#include \"%s\"\n" % header )
301    output.write( "#include <cxxtest/TestListener.h>\n" )
302    output.write( "#include <cxxtest/TestTracker.h>\n" )
303    output.write( "#include <cxxtest/TestRunner.h>\n" )
304    output.write( "#include <cxxtest/RealDescriptions.h>\n" )
305    output.write( "#include <cxxtest/TestMain.h>\n" )
306    if options.runner:
307        output.write( "#include <cxxtest/%s.h>\n" % options.runner )
308    if options.gui:
309        output.write( "#include <cxxtest/%s.h>\n" % options.gui )
310    output.write( "\n" )
311    wrotePreamble = 1
312
313def writeMain( output ):
314    '''Write the main() function for the test runner'''
315    if not (options.gui or options.runner):
316       return
317    output.write( 'int %s( int argc, char *argv[] ) {\n' % options.main )
318    output.write( ' int status;\n' )
319    if options.noStaticInit:
320        output.write( ' CxxTest::initialize();\n' )
321    if options.gui:
322        tester_t = "CxxTest::GuiTuiRunner<CxxTest::%s, CxxTest::%s> " % (options.gui, options.runner)
323    else:
324        tester_t = "CxxTest::%s" % (options.runner)
325    if options.xunit_printer:
326       output.write( '    std::ofstream ofstr("%s");\n' % options.xunit_file )
327       output.write( '    %s tmp(ofstr);\n' % tester_t )
328       output.write( '    CxxTest::RealWorldDescription::_worldName = "%s";\n' % options.world )
329    else:
330       output.write( '    %s tmp;\n' % tester_t )
331    output.write( '    status = CxxTest::Main<%s>( tmp, argc, argv );\n' % tester_t )
332    output.write( '    return status;\n')
333    output.write( '}\n' )
334
335
336def writeWorld( output ):
337    '''Write the world definitions'''
338    global wroteWorld
339    if wroteWorld: return
340    writePreamble( output )
341    writeSuites( output )
342    if options.root or not options.part:
343        writeRoot( output )
344        writeWorldDescr( output )
345    if options.noStaticInit:
346        writeInitialize( output )
347    wroteWorld = 1
348
349def writeSuites(output):
350    '''Write all TestDescriptions and SuiteDescriptions'''
351    for suite in suites:
352        writeInclude( output, suite['file'] )
353        if isGenerated(suite):
354            generateSuite( output, suite )
355        if isDynamic(suite):
356            writeSuitePointer( output, suite )
357        else:
358            writeSuiteObject( output, suite )
359        writeTestList( output, suite )
360        writeSuiteDescription( output, suite )
361        writeTestDescriptions( output, suite )
362
363def isGenerated(suite):
364    '''Checks whether a suite class should be created'''
365    return suite['generated']
366
367def isDynamic(suite):
368    '''Checks whether a suite is dynamic'''
369    return 'create' in suite
370
371def writeInclude(output, file):
372    '''Add #include "file" statement'''
373    global lastIncluded
374    if file == lastIncluded: return
375    output.writelines( [ '#include "', file, '"\n\n' ] )
376    lastIncluded = file
377
378def generateSuite( output, suite ):
379    '''Write a suite declared with CXXTEST_SUITE()'''
380    output.write( 'class %s : public CxxTest::TestSuite {\n' % suite['name'] )
381    output.write( 'public:\n' )
382    for line in suite['lines']:
383        output.write(line)
384    output.write( '};\n\n' )
385
386def writeSuitePointer( output, suite ):
387    '''Create static suite pointer object for dynamic suites'''
388    if options.noStaticInit:
389        output.write( 'static %s *%s;\n\n' % (suite['name'], suite['object']) )
390    else:
391        output.write( 'static %s *%s = 0;\n\n' % (suite['name'], suite['object']) )
392
393def writeSuiteObject( output, suite ):
394    '''Create static suite object for non-dynamic suites'''
395    output.writelines( [ "static ", suite['name'], " ", suite['object'], ";\n\n" ] )
396
397def writeTestList( output, suite ):
398    '''Write the head of the test linked list for a suite'''
399    if options.noStaticInit:
400        output.write( 'static CxxTest::List %s;\n' % suite['tlist'] )
401    else:
402        output.write( 'static CxxTest::List %s = { 0, 0 };\n' % suite['tlist'] )
403
404def writeWorldDescr( output ):
405    '''Write the static name of the world name'''
406    if options.noStaticInit:
407        output.write( 'const char* CxxTest::RealWorldDescription::_worldName;\n' )
408    else:
409        output.write( 'const char* CxxTest::RealWorldDescription::_worldName = "cxxtest";\n' )
410
411def writeTestDescriptions( output, suite ):
412    '''Write all test descriptions for a suite'''
413    for test in suite['tests']:
414        writeTestDescription( output, suite, test )
415
416def writeTestDescription( output, suite, test ):
417    '''Write test description object'''
418    output.write( 'static class %s : public CxxTest::RealTestDescription {\n' % test['class'] )
419    output.write( 'public:\n' )
420    if not options.noStaticInit:
421        output.write( ' %s() : CxxTest::RealTestDescription( %s, %s, %s, "%s" ) {}\n' %
422                      (test['class'], suite['tlist'], suite['dobject'], test['line'], test['name']) )
423    output.write( ' void runTest() { %s }\n' % runBody( suite, test ) )
424    output.write( '} %s;\n\n' % test['object'] )
425
426def runBody( suite, test ):
427    '''Body of TestDescription::run()'''
428    if isDynamic(suite): return dynamicRun( suite, test )
429    else: return staticRun( suite, test )
430
431def dynamicRun( suite, test ):
432    '''Body of TestDescription::run() for test in a dynamic suite'''
433    return 'if ( ' + suite['object'] + ' ) ' + suite['object'] + '->' + test['name'] + '();'
434
435def staticRun( suite, test ):
436    '''Body of TestDescription::run() for test in a non-dynamic suite'''
437    return suite['object'] + '.' + test['name'] + '();'
438
439def writeSuiteDescription( output, suite ):
440    '''Write SuiteDescription object'''
441    if isDynamic( suite ):
442        writeDynamicDescription( output, suite )
443    else:
444        writeStaticDescription( output, suite )
445
446def writeDynamicDescription( output, suite ):
447    '''Write SuiteDescription for a dynamic suite'''
448    output.write( 'CxxTest::DynamicSuiteDescription<%s> %s' % (suite['name'], suite['dobject']) )
449    if not options.noStaticInit:
450        output.write( '( %s, %s, "%s", %s, %s, %s, %s )' %
451                      (suite['cfile'], suite['line'], suite['name'], suite['tlist'],
452                       suite['object'], suite['create'], suite['destroy']) )
453    output.write( ';\n\n' )
454
455def writeStaticDescription( output, suite ):
456    '''Write SuiteDescription for a static suite'''
457    output.write( 'CxxTest::StaticSuiteDescription %s' % suite['dobject'] )
458    if not options.noStaticInit:
459        output.write( '( %s, %s, "%s", %s, %s )' %
460                      (suite['cfile'], suite['line'], suite['name'], suite['object'], suite['tlist']) )
461    output.write( ';\n\n' )
462
463def writeRoot(output):
464    '''Write static members of CxxTest classes'''
465    output.write( '#include <cxxtest/Root.cpp>\n' )
466
467def writeInitialize(output):
468    '''Write CxxTest::initialize(), which replaces static initialization'''
469    output.write( 'namespace CxxTest {\n' )
470    output.write( ' void initialize()\n' )
471    output.write( ' {\n' )
472    for suite in suites:
473        output.write( '  %s.initialize();\n' % suite['tlist'] )
474        if isDynamic(suite):
475            output.write( '  %s = 0;\n' % suite['object'] )
476            output.write( '  %s.initialize( %s, %s, "%s", %s, %s, %s, %s );\n' %
477                          (suite['dobject'], suite['cfile'], suite['line'], suite['name'],
478                           suite['tlist'], suite['object'], suite['create'], suite['destroy']) )
479        else:
480            output.write( '  %s.initialize( %s, %s, "%s", %s, %s );\n' %
481                          (suite['dobject'], suite['cfile'], suite['line'], suite['name'],
482                           suite['object'], suite['tlist']) )
483
484        for test in suite['tests']:
485            output.write( '  %s.initialize( %s, %s, %s, "%s" );\n' %
486                          (test['object'], suite['tlist'], suite['dobject'], test['line'], test['name']) )
487
488    output.write( ' }\n' )
489    output.write( '}\n' )
490
491man_template=Template("""CXXTESTGEN(1)
492=============
493:doctype: manpage
494
495
496NAME
497----
498cxxtestgen - performs test discovery to create a CxxTest test runner
499
500
501SYNOPSIS
502--------
503${usage}
504
505
506DESCRIPTION
507-----------
508${description}
509
510
511OPTIONS
512-------
513${options}
514
515
516EXIT STATUS
517-----------
518*0*::
519   Success
520
521*1*::
522   Failure (syntax or usage error; configuration error; document
523   processing failure; unexpected error).
524
525
526BUGS
527----
528See the CxxTest Home Page for the link to the CxxTest ticket repository.
529
530
531AUTHOR
532------
533CxxTest was originally written by Erez Volk. Many people have
534contributed to it.
535
536
537RESOURCES
538---------
539Home page: <http://cxxtest.com/>
540
541CxxTest User Guide: <http://cxxtest.com/cxxtest/doc/guide.html>
542
543
544
545COPYING
546-------
547Copyright (c) 2008 Sandia Corporation.  This software is distributed
548under the Lesser GNU General Public License (LGPL) v2.1
549""")
550
551def create_manpage():
552    """Write ASCIIDOC manpage file"""
553    parser = create_parser(asciidoc=True)
554    #
555    usage = parser.usage
556    description = parser.description
557    options=""
558    for opt in parser.option_list:
559        opts = opt._short_opts + opt._long_opts
560        optstr = '*' + ', '.join(opts) + '*'
561        if not opt.metavar is None:
562            optstr += "='%s'" % opt.metavar
563        optstr += '::\n'
564        options += optstr
565        #
566        options += opt.help
567        options += '\n\n'
568    #
569    OUTPUT = open('cxxtestgen.1.txt','w')
570    OUTPUT.write( man_template.substitute(usage=usage, description=description, options=options) )
571    OUTPUT.close()
572
573
574