approvalTests.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #!/usr/bin/env python3
  2. from __future__ import print_function
  3. import io
  4. import os
  5. import sys
  6. import subprocess
  7. import re
  8. import difflib
  9. import scriptCommon
  10. from scriptCommon import catchPath
  11. if os.name == 'nt':
  12. # Enable console colours on windows
  13. os.system('')
  14. rootPath = os.path.join(catchPath, 'projects/SelfTest/Baselines')
  15. langFilenameParser = re.compile(r'(.+\.[ch]pp)')
  16. filelocParser = re.compile(r'''
  17. .*/
  18. (.+\.[ch]pp) # filename
  19. (?::|\() # : is starting separator between filename and line number on Linux, ( on Windows
  20. ([0-9]*) # line number
  21. \)? # Windows also has an ending separator, )
  22. ''', re.VERBOSE)
  23. lineNumberParser = re.compile(r' line="[0-9]*"')
  24. hexParser = re.compile(r'\b(0[xX][0-9a-fA-F]+)\b')
  25. durationsParser = re.compile(r' time="[0-9]*\.[0-9]*"')
  26. sonarqubeDurationParser = re.compile(r' duration="[0-9]+"')
  27. timestampsParser = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z')
  28. versionParser = re.compile(r'Catch v[0-9]+\.[0-9]+\.[0-9]+(-develop\.[0-9]+)?')
  29. nullParser = re.compile(r'\b(__null|nullptr)\b')
  30. exeNameParser = re.compile(r'''
  31. \b
  32. (CatchSelfTest|SelfTest) # Expected executable name
  33. (?:.exe)? # Executable name contains .exe on Windows.
  34. \b
  35. ''', re.VERBOSE)
  36. # This is a hack until something more reasonable is figured out
  37. specialCaseParser = re.compile(r'file\((\d+)\)')
  38. # errno macro expands into various names depending on platform, so we need to fix them up as well
  39. errnoParser = re.compile(r'''
  40. \(\*__errno_location\ \(\)\)
  41. |
  42. \(\*__error\(\)\)
  43. |
  44. \(\*_errno\(\)\)
  45. ''', re.VERBOSE)
  46. sinceEpochParser = re.compile(r'\d+ .+ since epoch')
  47. infParser = re.compile(r'''
  48. \(\(float\)\(1e\+300\ \*\ 1e\+300\)\) # MSVC INFINITY macro
  49. |
  50. \(__builtin_inff\(\)\) # Linux (ubuntu) INFINITY macro
  51. |
  52. \(__builtin_inff\ \(\)\) # Fedora INFINITY macro
  53. |
  54. __builtin_huge_valf\(\) # OSX macro
  55. ''', re.VERBOSE)
  56. nanParser = re.compile(r'''
  57. \(\(float\)\(\(\(float\)\(1e\+300\ \*\ 1e\+300\)\)\ \*\ 0\.0F\)\) # MSVC NAN macro
  58. |
  59. \(\(float\)\(INFINITY\ \*\ 0\.0F\)\) # Yet another MSVC NAN macro
  60. |
  61. \(__builtin_nanf\ \(""\)\) # Linux (ubuntu) NAN macro
  62. |
  63. __builtin_nanf\("0x<hex\ digits>"\) # The weird content of the brackets is there because a different parser has already ran before this one
  64. ''', re.VERBOSE)
  65. if len(sys.argv) == 2:
  66. cmdPath = sys.argv[1]
  67. else:
  68. cmdPath = os.path.join(catchPath, scriptCommon.getBuildExecutable())
  69. overallResult = 0
  70. def diffFiles(fileA, fileB):
  71. with io.open(fileA, 'r', encoding='utf-8', errors='surrogateescape') as file:
  72. aLines = [line.rstrip() for line in file.readlines()]
  73. with io.open(fileB, 'r', encoding='utf-8', errors='surrogateescape') as file:
  74. bLines = [line.rstrip() for line in file.readlines()]
  75. shortenedFilenameA = fileA.rsplit(os.sep, 1)[-1]
  76. shortenedFilenameB = fileB.rsplit(os.sep, 1)[-1]
  77. diff = difflib.unified_diff(aLines, bLines, fromfile=shortenedFilenameA, tofile=shortenedFilenameB, n=0)
  78. return [line for line in diff if line[0] in ('+', '-')]
  79. def normalizeFilepath(line):
  80. if catchPath in line:
  81. # make paths relative to Catch root
  82. line = line.replace(catchPath + os.sep, '')
  83. m = langFilenameParser.match(line)
  84. if m:
  85. filepath = m.group(0)
  86. # go from \ in windows paths to /
  87. filepath = filepath.replace('\\', '/')
  88. # remove start of relative path
  89. filepath = filepath.replace('../', '')
  90. line = line[:m.start()] + filepath + line[m.end():]
  91. return line
  92. def filterLine(line, isCompact):
  93. line = normalizeFilepath(line)
  94. # strip source line numbers
  95. m = filelocParser.match(line)
  96. if m:
  97. # note that this also strips directories, leaving only the filename
  98. filename, lnum = m.groups()
  99. lnum = ":<line number>" if lnum else ""
  100. line = filename + lnum + line[m.end():]
  101. else:
  102. line = lineNumberParser.sub(" ", line)
  103. if isCompact:
  104. line = line.replace(': FAILED', ': failed')
  105. line = line.replace(': PASSED', ': passed')
  106. # strip Catch version number
  107. line = versionParser.sub("<version>", line)
  108. # replace *null* with 0
  109. line = nullParser.sub("0", line)
  110. # strip executable name
  111. line = exeNameParser.sub("<exe-name>", line)
  112. # strip hexadecimal numbers (presumably pointer values)
  113. line = hexParser.sub("0x<hex digits>", line)
  114. # strip durations and timestamps
  115. line = durationsParser.sub(' time="{duration}"', line)
  116. line = sonarqubeDurationParser.sub(' duration="{duration}"', line)
  117. line = timestampsParser.sub('{iso8601-timestamp}', line)
  118. line = specialCaseParser.sub('file:\g<1>', line)
  119. line = errnoParser.sub('errno', line)
  120. line = sinceEpochParser.sub('{since-epoch-report}', line)
  121. line = infParser.sub('INFINITY', line)
  122. line = nanParser.sub('NAN', line)
  123. return line
  124. def approve(baseName, args):
  125. global overallResult
  126. args[0:0] = [cmdPath]
  127. if not os.path.exists(cmdPath):
  128. raise Exception("Executable doesn't exist at " + cmdPath)
  129. baselinesPath = os.path.join(rootPath, '{0}.approved.txt'.format(baseName))
  130. rawResultsPath = os.path.join(rootPath, '_{0}.tmp'.format(baseName))
  131. filteredResultsPath = os.path.join(rootPath, '{0}.unapproved.txt'.format(baseName))
  132. f = open(rawResultsPath, 'w')
  133. subprocess.call(args, stdout=f, stderr=f)
  134. f.close()
  135. rawFile = io.open(rawResultsPath, 'r', encoding='utf-8', errors='surrogateescape')
  136. filteredFile = io.open(filteredResultsPath, 'w', encoding='utf-8', errors='surrogateescape')
  137. for line in rawFile:
  138. filteredFile.write(filterLine(line, 'compact' in baseName).rstrip() + "\n")
  139. filteredFile.close()
  140. rawFile.close()
  141. os.remove(rawResultsPath)
  142. print()
  143. print(baseName + ":")
  144. if os.path.exists(baselinesPath):
  145. diffResult = diffFiles(baselinesPath, filteredResultsPath)
  146. if diffResult:
  147. print('\n'.join(diffResult))
  148. print(" \n****************************\n \033[91mResults differed")
  149. if len(diffResult) > overallResult:
  150. overallResult = len(diffResult)
  151. else:
  152. os.remove(filteredResultsPath)
  153. print(" \033[92mResults matched")
  154. print("\033[0m")
  155. else:
  156. print(" first approval")
  157. if overallResult == 0:
  158. overallResult = 1
  159. print("Running approvals against executable:")
  160. print(" " + cmdPath)
  161. # ## Keep default reporters here ##
  162. # Standard console reporter
  163. approve("console.std", ["~[!nonportable]~[!benchmark]~[approvals]", "--order", "lex", "--rng-seed", "1"])
  164. # console reporter, include passes, warn about No Assertions
  165. approve("console.sw", ["~[!nonportable]~[!benchmark]~[approvals]", "-s", "-w", "NoAssertions", "--order", "lex", "--rng-seed", "1"])
  166. # console reporter, include passes, warn about No Assertions, limit failures to first 4
  167. approve("console.swa4", ["~[!nonportable]~[!benchmark]~[approvals]", "-s", "-w", "NoAssertions", "-x", "4", "--order", "lex", "--rng-seed", "1"])
  168. # junit reporter, include passes, warn about No Assertions
  169. approve("junit.sw", ["~[!nonportable]~[!benchmark]~[approvals]", "-s", "-w", "NoAssertions", "-r", "junit", "--order", "lex", "--rng-seed", "1"])
  170. # xml reporter, include passes, warn about No Assertions
  171. approve("xml.sw", ["~[!nonportable]~[!benchmark]~[approvals]", "-s", "-w", "NoAssertions", "-r", "xml", "--order", "lex", "--rng-seed", "1"])
  172. # compact reporter, include passes, warn about No Assertions
  173. approve('compact.sw', ['~[!nonportable]~[!benchmark]~[approvals]', '-s', '-w', 'NoAssertions', '-r', 'compact', '--order', 'lex', "--rng-seed", "1"])
  174. # sonarqube reporter, include passes, warn about No Assertions
  175. approve("sonarqube.sw", ["~[!nonportable]~[!benchmark]~[approvals]", "-s", "-w", "NoAssertions", "-r", "sonarqube", "--order", "lex", "--rng-seed", "1"])
  176. if overallResult != 0:
  177. print("If these differences are expected, run approve.py to approve new baselines.")
  178. exit(overallResult)