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