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
11
12import codecs
13import re
14#import sys
15#import getopt
16#import glob
17from cxxtest.cxxtest_misc import abort
18
19# Global variables
20suites = []
21suite = None
22inBlock = 0
23options=None
24
25def scanInputFiles(files, _options):
26    '''Scan all input files for test suites'''
27    global options
28    options=_options
29    for file in files:
30        scanInputFile(file)
31    global suites
32    if len(suites) is 0 and not options.root:
33        abort( 'No tests defined' )
34    return [options,suites]
35
36lineCont_re = re.compile('(.*)\\\s*$')
37def scanInputFile(fileName):
38    '''Scan single input file for test suites'''
39    # mode 'rb' is problematic in python3 - byte arrays don't behave the same as
40    # strings.
41    # As far as the choice of the default encoding: utf-8 chews through
42    # everything that the previous ascii codec could, plus most of new code.
43    # TODO: figure out how to do this properly - like autodetect encoding from
44    # file header.
45    file = codecs.open(fileName, mode='r', encoding='utf-8')
46    prev = ""
47    lineNo = 0
48    contNo = 0
49    while 1:
50        line = file.readline()
51        if not line:
52            break
53        lineNo += 1
54
55        m = lineCont_re.match(line)
56        if m:
57            prev += m.group(1) + " "
58            contNo += 1
59        else:
60            scanInputLine( fileName, lineNo - contNo, prev + line )
61            contNo = 0
62            prev = ""
63    if contNo:
64        scanInputLine( fileName, lineNo - contNo, prev + line )
65
66    closeSuite()
67    file.close()
68
69def scanInputLine( fileName, lineNo, line ):
70    '''Scan single input line for interesting stuff'''
71    scanLineForExceptionHandling( line )
72    scanLineForStandardLibrary( line )
73
74    scanLineForSuiteStart( fileName, lineNo, line )
75
76    global suite
77    if suite:
78        scanLineInsideSuite( suite, lineNo, line )
79
80def scanLineInsideSuite( suite, lineNo, line ):
81    '''Analyze line which is part of a suite'''
82    global inBlock
83    if lineBelongsToSuite( suite, lineNo, line ):
84        scanLineForTest( suite, lineNo, line )
85        scanLineForCreate( suite, lineNo, line )
86        scanLineForDestroy( suite, lineNo, line )
87
88def lineBelongsToSuite( suite, lineNo, line ):
89    '''Returns whether current line is part of the current suite.
90    This can be false when we are in a generated suite outside of CXXTEST_CODE() blocks
91    If the suite is generated, adds the line to the list of lines'''
92    if not suite['generated']:
93        return 1
94
95    global inBlock
96    if not inBlock:
97        inBlock = lineStartsBlock( line )
98    if inBlock:
99        inBlock = addLineToBlock( suite, lineNo, line )
100    return inBlock
101
102
103std_re = re.compile( r"\b(std\s*::|CXXTEST_STD|using\s+namespace\s+std\b|^\s*\#\s*include\s+<[a-z0-9]+>)" )
104def scanLineForStandardLibrary( line ):
105    '''Check if current line uses standard library'''
106    global options
107    if not options.haveStandardLibrary and std_re.search(line):
108        if not options.noStandardLibrary:
109            options.haveStandardLibrary = 1
110
111exception_re = re.compile( r"\b(throw|try|catch|TSM?_ASSERT_THROWS[A-Z_]*)\b" )
112def scanLineForExceptionHandling( line ):
113    '''Check if current line uses exception handling'''
114    global options
115    if not options.haveExceptionHandling and exception_re.search(line):
116        if not options.noExceptionHandling:
117            options.haveExceptionHandling = 1
118
119classdef = '(?:::\s*)?(?:\w+\s*::\s*)*\w+'
120baseclassdef = '(?:public|private|protected)\s+%s' % (classdef,)
121general_suite = r"\bclass\s+(%s)\s*:(?:\s*%s\s*,)*\s*public\s+" \
122                % (classdef, baseclassdef,)
123testsuite = '(?:(?:::)?\s*CxxTest\s*::\s*)?TestSuite'
124suites_re = { re.compile( general_suite + testsuite ) : None }
125generatedSuite_re = re.compile( r'\bCXXTEST_SUITE\s*\(\s*(\w*)\s*\)' )
126def scanLineForSuiteStart( fileName, lineNo, line ):
127    '''Check if current line starts a new test suite'''
128    for i in list(suites_re.items()):
129        m = i[0].search( line )
130        if m:
131            suite = startSuite( m.group(1), fileName, lineNo, 0 )
132            if i[1] is not None:
133                for test in i[1]['tests']:
134                    addTest(suite, test['name'], test['line'])
135            break
136    m = generatedSuite_re.search( line )
137    if m:
138        sys.stdout.write( "%s:%s: Warning: Inline test suites are deprecated.\n" % (fileName, lineNo) )
139        startSuite( m.group(1), fileName, lineNo, 1 )
140
141def startSuite( name, file, line, generated ):
142    '''Start scanning a new suite'''
143    global suite
144    closeSuite()
145    object_name = name.replace(':',"_")
146    suite = { 'name'         : name,
147              'file'         : file,
148              'cfile'        : cstr(file),
149              'line'         : line,
150              'generated'    : generated,
151              'object'       : 'suite_%s' % object_name,
152              'dobject'      : 'suiteDescription_%s' % object_name,
153              'tlist'        : 'Tests_%s' % object_name,
154              'tests'        : [],
155              'lines'        : [] }
156    suites_re[re.compile( general_suite + name )] = suite
157    return suite
158
159def lineStartsBlock( line ):
160    '''Check if current line starts a new CXXTEST_CODE() block'''
161    return re.search( r'\bCXXTEST_CODE\s*\(', line ) is not None
162
163test_re = re.compile( r'^([^/]|/[^/])*\bvoid\s+([Tt]est\w+)\s*\(\s*(void)?\s*\)' )
164def scanLineForTest( suite, lineNo, line ):
165    '''Check if current line starts a test'''
166    m = test_re.search( line )
167    if m:
168        addTest( suite, m.group(2), lineNo )
169
170def addTest( suite, name, line ):
171    '''Add a test function to the current suite'''
172    test = { 'name'   : name,
173             'suite'  : suite,
174             'class'  : 'TestDescription_%s_%s' % (suite['object'], name),
175             'object' : 'testDescription_%s_%s' % (suite['object'], name),
176             'line'   : line,
177             }
178    suite['tests'].append( test )
179
180def addLineToBlock( suite, lineNo, line ):
181    '''Append the line to the current CXXTEST_CODE() block'''
182    line = fixBlockLine( suite, lineNo, line )
183    line = re.sub( r'^.*\{\{', '', line )
184
185    e = re.search( r'\}\}', line )
186    if e:
187        line = line[:e.start()]
188    suite['lines'].append( line )
189    return e is None
190
191def fixBlockLine( suite, lineNo, line):
192    '''Change all [E]TS_ macros used in a line to _[E]TS_ macros with the correct file/line'''
193    return re.sub( r'\b(E?TSM?_(ASSERT[A-Z_]*|FAIL))\s*\(',
194                   r'_\1(%s,%s,' % (suite['cfile'], lineNo),
195                   line, 0 )
196
197create_re = re.compile( r'\bstatic\s+\w+\s*\*\s*createSuite\s*\(\s*(void)?\s*\)' )
198def scanLineForCreate( suite, lineNo, line ):
199    '''Check if current line defines a createSuite() function'''
200    if create_re.search( line ):
201        addSuiteCreateDestroy( suite, 'create', lineNo )
202
203destroy_re = re.compile( r'\bstatic\s+void\s+destroySuite\s*\(\s*\w+\s*\*\s*\w*\s*\)' )
204def scanLineForDestroy( suite, lineNo, line ):
205    '''Check if current line defines a destroySuite() function'''
206    if destroy_re.search( line ):
207        addSuiteCreateDestroy( suite, 'destroy', lineNo )
208
209def cstr( s ):
210    '''Convert a string to its C representation'''
211    return '"' + s.replace( '\\', '\\\\' ) + '"'
212
213
214def addSuiteCreateDestroy( suite, which, line ):
215    '''Add createSuite()/destroySuite() to current suite'''
216    if which in suite:
217        abort( '%s:%s: %sSuite() already declared' % ( suite['file'], str(line), which ) )
218    suite[which] = line
219
220def closeSuite():
221    '''Close current suite and add it to the list if valid'''
222    global suite
223    if suite is not None:
224        if len(suite['tests']) is not 0:
225            verifySuite(suite)
226            rememberSuite(suite)
227        suite = None
228
229def verifySuite(suite):
230    '''Verify current suite is legal'''
231    if 'create' in suite and 'destroy' not in suite:
232        abort( '%s:%s: Suite %s has createSuite() but no destroySuite()' %
233               (suite['file'], suite['create'], suite['name']) )
234    elif 'destroy' in suite and 'create' not in suite:
235        abort( '%s:%s: Suite %s has destroySuite() but no createSuite()' %
236               (suite['file'], suite['destroy'], suite['name']) )
237
238def rememberSuite(suite):
239    '''Add current suite to list'''
240    global suites
241    suites.append( suite )
242
243