runTest.py
Go to the documentation of this file.
1 #! /usr/bin/env python3
2 #------------------------------------------------------------------------------
3 
4 import os
5 import sys
6 import subprocess
7 import re
8 import argparse
9 import tempfile
10 import datetime
11 import difflib
12 import platform
13 #------------------------------------------------------------------------------
14 
15 mypath = '.'
16 
17 def runCmd(cmd, data=None):
18  if input is None:
19  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
20  stdout, stderr = p.communicate()
21  else:
22  p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23  stdout, stderr = p.communicate(input=data)
24 
25  return stdout.decode('utf-8'), stderr.decode('utf-8'), p.returncode
26 #------------------------------------------------------------------------------
27 
28 def cleanStderr(stderr, fileName=None):
29  if fileName is not None:
30  stderr = stderr.replace(fileName, '.tmp.cpp')
31  else:
32  stderr = re.sub('(.*).cpp:', '.tmp:', stderr)
33 # stderr = re.sub('[\n ](.*).cpp:', '\\1%s:' %(fileName), stderr)
34 
35  # Replace paths, as for example, the STL path differs from a local build to Travis-CI at least for macOS
36  stderr = re.sub('/(.*)/(.*?:[0-9]+):', '... \\2:', stderr)
37  stderr = re.sub('RecoveryExpr 0x[a-f0-9]+ ', 'RecoveryExpr ', stderr)
38 
39  return stderr
40 #------------------------------------------------------------------------------
41 
42 def testCompare(tmpFileName, stdout, expectFile, f, args, time):
43  expect = open(expectFile, 'r', encoding='utf-8').read()
44 
45  # align line-endings under Windows to Unix-style
46  if os.name == 'nt':
47  stdout = stdout.replace('\r\n', '\n')
48 
49  if stdout != expect:
50  print('[FAILED] %s - %s' %(f, time))
51 
52  for line in difflib.unified_diff(expect.splitlines(keepends=True), stdout.splitlines(keepends=True), fromfile=expectFile, tofile='stdout', n=3):
53  print('%s' %((line[1:] if line.startswith(' ') else line) ), end='')
54  else:
55  print('[PASSED] %-50s - %s' %(f, time))
56  return True
57 
58  return False
59 #------------------------------------------------------------------------------
60 
61 def testCompile(tmpFileName, f, args, fileName, cppStd):
62  if os.name == 'nt':
63  cppStd = cppStd.replace('-std=', '/std:')
64  cppStd = cppStd.replace('2a', 'latest')
65 
66  cmd = [args['cxx'], cppStd, '-D__cxa_guard_acquire(x)=true', '-D__cxa_guard_release(x)', '-D__cxa_guard_abort(x)', '-I', os.getcwd()]
67 
68  if os.name != 'nt':
69  arch = platform.architecture()[0]
70  if (arch != '64bit') or ((arch == '64bit') and (sys.platform == 'darwin')):
71  cmd.append('-m64')
72  else:
73  cmd.extend(['/nologo', '/EHsc', '/IGNORE:C4335']) # C4335: mac file format detected. EHsc assume only C++ functions throw exceptions.
74 
75  # GCC seems to dislike empty ''
76  if '-std=c++98' == cppStd:
77  cmd += ['-Dalignas(x)=']
78 
79  cmd += ['-c', tmpFileName]
80 
81  stdout, stderr, returncode = runCmd(cmd)
82 
83  compileErrorFile = os.path.join(mypath, fileName + '.cerr')
84  if 0 != returncode:
85  if os.path.isfile(compileErrorFile):
86  ce = open(compileErrorFile, 'r', encoding='utf-8').read()
87  stderr = cleanStderr(stderr, tmpFileName)
88 
89  if ce == stderr:
90  print(f'[PASSED] Compile: {f}')
91  return True, None
92 
93  compileErrorFile = os.path.join(mypath, fileName + '.ccerr')
94  if os.path.isfile(compileErrorFile):
95  ce = open(compileErrorFile, 'r', encoding='utf-8').read()
96  stderr = stderr.replace(tmpFileName, '.tmp.cpp')
97 
98  if ce == stderr:
99  print('f[PASSED] Compile: {f}')
100  return True, None
101 
102  print(f'[ERROR] Compile failed: {f}')
103  print(stderr)
104  else:
105  if os.path.isfile(compileErrorFile):
106  print('unused file: %s' %(compileErrorFile))
107 
108  ext = 'obj' if os.name == 'nt' else 'o'
109 
110  objFileName = '%s.%s' %(os.path.splitext(os.path.basename(tmpFileName))[0], ext)
111  os.remove(objFileName)
112 
113  print(f'[PASSED] Compile: {f}')
114  return True, None
115 
116  return False, stderr
117 #------------------------------------------------------------------------------
118 
119 
120 def main():
121  parser = argparse.ArgumentParser(description='Description of your program')
122  parser.add_argument('--insights', help='C++ Insights binary', required=True)
123  parser.add_argument('--cxx', help='C++ compiler to used', default='/usr/local/clang-current/bin/clang++')
124  parser.add_argument('--failure-is-ok', help='Failing tests are ok', default=False, action='store_true')
125  parser.add_argument('--update-tests', help='Update failing tests', default=False, action='store_true')
126  parser.add_argument('--std', help='C++ Standard to used', default='c++17')
127  parser.add_argument('--use-libcpp', help='Use libst++', default=False, action='store_true')
128  parser.add_argument('--llvm-prof-dir', help='LLVM profiles data dir', default='')
129  parser.add_argument('args', nargs=argparse.REMAINDER)
130  args = vars(parser.parse_args())
131 
132  insightsPath = args['insights']
133  remainingArgs = args['args']
134  bFailureIsOk = args['failure_is_ok']
135  bUpdateTests = args['update_tests']
136  defaultCppStd = f"-std={args['std']}"
137 
138  if args['llvm_prof_dir'] != '':
139  os.environ['LLVM_PROFILE_FILE'] = os.path.join(args['llvm_prof_dir'], 'prof%p.profraw')
140 
141  if 0 == len(remainingArgs):
142  cppFiles = [f for f in os.listdir(mypath) if (os.path.isfile(os.path.join(mypath, f)) and f.endswith('.cpp'))]
143  else:
144  cppFiles = remainingArgs
145 
146  filesPassed = 0
147  missingExpected = 0
148  crashes = 0
149  ret = 0
150 
151  regEx = re.compile('.*cmdline:(.*)')
152  regExInsights = re.compile('.*cmdlineinsights:(.*)')
153 
154  for f in sorted(cppFiles):
155  fileName = os.path.splitext(f)[0]
156  expectFile = os.path.join(mypath, fileName + '.expect')
157  ignoreFile = os.path.join(mypath, fileName + '.ignore')
158  cppStd = defaultCppStd
159  insightsOpts = ''
160 
161  fh = open(f, 'r', encoding='utf-8')
162  fileHeader = fh.readline()
163  fileHeader += fh.readline()
164  m = regEx.search(fileHeader)
165  if m is not None:
166  cppStd = m.group(1)
167 
168  m = regExInsights.search(fileHeader)
169  if m is not None:
170  insightsOpts = m.group(1).split(' ')
171 
172  if not os.path.isfile(expectFile) and not os.path.isfile(ignoreFile):
173  print(f'Missing expect/ignore for: {f}')
174  missingExpected += 1
175  continue
176 
177  if os.path.isfile(ignoreFile):
178  print(f'Ignoring: {f}')
179  filesPassed += 1
180  continue
181 
182  cmd = [insightsPath, f]
183 
184  if args['use_libcpp']:
185  cmd.append('-use-libc++')
186 
187 
188  if len(insightsOpts):
189  cmd.extend(insightsOpts)
190 
191  cmd.extend(['--', cppStd, '-m64'])
192 
193  begin = datetime.datetime.now()
194  stdout, stderr, returncode = runCmd(cmd)
195  end = datetime.datetime.now()
196 
197  if 0 != returncode:
198  compileErrorFile = os.path.join(mypath, fileName + '.cerr')
199  if os.path.isfile(compileErrorFile):
200  ce = open(compileErrorFile, 'r', encoding='utf-8').read()
201 
202  # Linker errors name the tmp file and not the .tmp.cpp, replace the name here to be able to suppress
203  # these errors.
204  ce = re.sub('(.*).cpp:', '.tmp:', ce)
205  ce = re.sub('(.*).cpp.', '.tmp:', ce)
206  stderr = re.sub('(Error while processing.*.cpp.)', '', stderr)
207  stderr = cleanStderr(stderr)
208 
209  # The cerr output matches and the return code says that we hit a compile error, accept it as passed
210  if ((ce == stderr) and (1 == returncode)) or os.path.exists(os.path.join(mypath, fileName + '.failure')):
211  print(f'[PASSED] Transform: {f}')
212  filesPassed += 1
213  continue
214  else:
215  print(f'[ERROR] Transform: {f}')
216  ret = 1
217 
218  crashes += 1
219  print(f'Insight crashed for: {f} with: {returncode}')
220  print(stderr)
221 
222  if not bUpdateTests:
223  continue
224 
225  fd, tmpFileName = tempfile.mkstemp('.cpp')
226  try:
227  with os.fdopen(fd, 'w', encoding='utf-8') as tmp:
228  # write the data to the temp file
229  tmp.write(stdout)
230 
231  equal = testCompare(tmpFileName, stdout, expectFile, f, args, end-begin)
232  bCompiles, stderr = testCompile(tmpFileName, f, args, fileName, cppStd)
233  compileErrorFile = os.path.join(mypath, fileName + '.cerr')
234 
235 
236  if (bCompiles and equal) or bFailureIsOk:
237  filesPassed += 1
238  elif bUpdateTests:
239  if bCompiles and not equal:
240  open(expectFile, 'w', encoding='utf-8').write(stdout)
241  print('Updating test')
242  elif not bCompiles and os.path.exists(compileErrorFile):
243  open(expectFile, 'w', encoding='utf-8').write(stdout)
244  open(compileErrorFile, 'w', encoding='utf-8').write(stderr)
245  print('Updating test cerr')
246 
247 
248  finally:
249  os.remove(tmpFileName)
250 
251 
252 
253  expectedToPass = len(cppFiles)-missingExpected
254  print('-----------------------------------------------------------------')
255  print(f'Tests passed: {filesPassed}/{expectedToPass}')
256 
257  print(f'Insights crashed: {crashes}')
258  print(f'Missing expected files: {missingExpected}')
259 
260  passed = (0 == missingExpected) and (expectedToPass == filesPassed)
261 
262  return (passed is False) # note bash expects 0 for ok
263 #------------------------------------------------------------------------------
264 
265 
266 sys.exit(main())
267 #------------------------------------------------------------------------------
268 
def testCompare(tmpFileName, stdout, expectFile, f, args, time)
Definition: runTest.py:42
def main()
Definition: runTest.py:120
def runCmd(cmd, data=None)
Definition: runTest.py:17
def testCompile(tmpFileName, f, args, fileName, cppStd)
Definition: runTest.py:61
def cleanStderr(stderr, fileName=None)
Definition: runTest.py:28