#!/usr/bin/env python3 # # Cppcheck - A tool for static C/C++ code analysis # Copyright (C) 2007-2021 Cppcheck team. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Extract test cases information from Cppcheck test file """ import os import sys import re def get_includes(code): includes = (('alloca','alloca.h'), ('NULL','cstddef'), ('size_t','cstddef'), ('free','cstdlib'), ('malloc','cstdlib'), ('realloc','cstdlib'), ('memcpy','cstring'), ('stdin','cstdio'), ('strcat','cstring'), ('strchr','cstring'), ('strcpy','cstring'), ('strlen','cstring'), ('strncat','cstring'), ('strncpy','cstring'), ('std::cout','iostream'), ('std::pair','utility'), ('std::shared_ptr','memory'), ('std::string','string'), ('std::unique_ptr','memory'), ('std::vector','vector')) ret = '' for i in includes: if i[0] in code: include_header = '#include <%s>' % i[1] if include_header not in ret: ret += include_header + '\n' return ret def tweak_expected(expected, start_code): if start_code is None or start_code == '': return expected res = re.match(r'[^(]*\[([^:\]]+):([0-9]+)\](.*)', expected) if res is None: return expected lines = len(start_code[:-1].split('\n')) return '[%s:%i]%s' % (res.group(1), lines + int(res.group(2)), res.group(3)) class Extract: """ Read Cppcheck test file and create data representation """ # array that stores all the test cases nodes = [] def parseFile(self, filename): """ parse test file and add info to the nodes variable """ name = '[0-9a-zA-Z_]+' string = '\\"(.+)\\"' testclass = None functionName = None code = None start_code = None disable = False for line in open(filename, 'r'): # testclass starts res = re.match('class (' + name + ')', line) if res is not None: testclass = res.group(1) # end of testclass if re.match('};', line) is not None: testclass = None # function start res = re.match('\\s+void (' + name + ')\\(\\)', line) if res is not None: functionName = res.group(1) start_code = None elif re.match('\\s+}', line) is not None: functionName = None # extracttests commands.. res = re.match(r'\s*//\s*extracttests.start:(.*)', line) if res is not None: start_code = res.group(1).replace('\\n', '\n') + '\n' elif line.find('extracttests.disable') >= 0: disable = True elif line.find('extracttests.enable') >= 0: disable = False if functionName is None or disable: continue # check for f in check_function: res = re.match('\\s+' + f + '\\(' + string, line) if res is not None: code = res.group(1) break # code.. if code is not None: res = re.match('\\s+' + string, line) if res is not None: if line.find('",') > line.find('"'): code = None continue code = code + res.group(1) if res.group(1).find('"') > 0: code = None # assert res = re.match('\\s+ASSERT_EQUALS\\(\\"([^"]*)\\",', line) if res is not None and code is not None: if start_code: includes = get_includes(start_code + code) code = includes + start_code + code expected = tweak_expected(res.group(1), includes + start_code) else: includes = get_includes(code) code = includes + code expected = tweak_expected(res.group(1), includes) node = {'testclass': testclass, 'functionName': functionName, 'code': code.replace("\\\\", "\\"), 'expected': expected} self.nodes.append(node) code = None elif re.match('\\s+[TOD_]*ASSERT', line) is not None: code = None def strtoxml(s): """Convert string to xml/html format""" return s.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>') def trimname(name): """Trim test name. Trailing underscore and digits are removed""" while name[-1].isdigit(): name = name[:-1] if name[-1] == '_': name = name[:-1] return name def writeHtmlFile(nodes, functionName, filename, errorsOnly): """Write html file for a function name""" fout = open(filename, 'w') fout.write('\n') fout.write('\n') fout.write(' \n') fout.write('\n') fout.write('\n') fout.write('Home -- ') if errorsOnly: fout.write('All test cases') else: fout.write( 'Error test cases') fout.write('

') testclass = None num = 0 for node in nodes: if errorsOnly and node['expected'] == '': continue if trimname(node['functionName']) == functionName: num = num + 1 if not testclass: testclass = node['testclass'] fout.write( '

' + node['testclass'] + '::' + functionName + '

') fout.write('\n') fout.write( ' \n') fout.write(' ') fout.write('') fout.write( '') fout.write('\n') if testclass is not None: fout.write('
NrCodeExpected
' + str(num) + '
' + strtoxml(
                node['code']).replace('\\n', '\n') + '
' + strtoxml(node['expected']).replace('\\n', '
') + '
\n') fout.write('\n') fout.close() if len(sys.argv) <= 1 or '--help' in sys.argv: print('Extract test cases from test file') print( 'Syntax: extracttests.py [--html=folder] [--xml] [--code=folder] [--only-tp] [--check-function=check] path/testfile.cpp') sys.exit(0) # parse command line xml = False filename = None htmldir = None codedir = None onlyTP = None check_function = ['check[A-Za-z0-9_]*'] for arg in sys.argv[1:]: if arg == '--xml': xml = True elif arg == '--only-tp': onlyTP = True elif arg.startswith('--html='): htmldir = arg[7:] elif arg.startswith('--code='): codedir = arg[7:] elif arg.endswith('.cpp'): filename = arg elif arg.startswith('--check-function='): check_function.append(arg[17:]) else: print('Invalid option: ' + arg) sys.exit(1) # extract test cases if filename is not None: # parse test file e = Extract() e.parseFile(filename) # generate output if xml: print('') print('') count = 0 for node in e.nodes: s = ' ') elif htmldir is not None: if not htmldir.endswith('/'): htmldir += '/' if not os.path.exists(htmldir): os.mkdir(htmldir) findex = open(htmldir + 'index.htm', 'w') findex.write('\n') findex.write('\n') findex.write(' \n') findex.write('\n') findex.write('\n') findex.write('

' + filename + '

\n') functionNames = [] for node in e.nodes: functionname = trimname(node['functionName']) if functionname not in functionNames: functionNames.append(functionname) functionNames.sort() findex.write('\n') findex.write(' \n') for functionname in functionNames: findex.write(' ') numall = 0 numerr = 0 for node in e.nodes: if trimname(node['functionName']) == functionname: numall = numall + 1 if node['expected'] != '': numerr = numerr + 1 if numerr == 0: findex.write('') else: findex.write('') findex.write('') findex.write('\n') findex.write('
NameErrorsAll
' + functionname + '
0
' + str(numerr) + '
' + str(numall) + '
\n') findex.write('') findex.close() # create files for each functionName for functionName in functionNames: writeHtmlFile(e.nodes, functionName, htmldir + 'errors-' + functionName + '.htm', True) writeHtmlFile(e.nodes, functionName, htmldir + 'all-' + functionName + '.htm', False) elif codedir: testnum = 0 if not codedir.endswith('/'): codedir = codedir + '/' if not os.path.exists(codedir): os.mkdir(codedir) testfile = os.path.splitext(os.path.basename(filename))[0] for node in e.nodes: if onlyTP and node['expected'] == '': continue testnum = testnum + 1 functionName = node['functionName'] code = node['code'] code = code.replace('\\n', '\n') code = code.replace('\\"', '"') expected = node['expected'] if expected.endswith('\\n'): expected = expected[:-2] filename = '%s-%03i-%s.cpp' % (testfile, testnum, functionName) # comment error res = re.match(r'[^(]*\[([^:\]]+):([0-9]+)\]: \([a-z, ]+\) (.*)', expected) if res: line_number = int(res.group(2)) - 1 lines = code.split('\n') if len(lines) > line_number: lines[line_number] += ' // ' + res.group(3) code = '\n'.join(lines) else: print('filename:%s expected:%s' % (filename, expected)) # source code with open(codedir + filename, 'w') as fout: fout.write(code + '\n') else: for node in e.nodes: print(node['functionName'])