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