approvalTests.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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, 'tests/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. # Note: junit must serialize time with 3 (or or less) decimal places
  26. # before generalizing this parser, make sure that this is checked
  27. # in other places too.
  28. junitDurationsParser = re.compile(r' time="[0-9]+\.[0-9]{3}"')
  29. durationParser = re.compile(r''' duration=['"][0-9]+['"]''')
  30. timestampsParser = re.compile(r'\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z')
  31. versionParser = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+(-\w*\.[0-9]+)?')
  32. nullParser = re.compile(r'\b(__null|nullptr)\b')
  33. exeNameParser = re.compile(r'''
  34. \b
  35. SelfTest # Expected executable name
  36. (?:.exe)? # Executable name contains .exe on Windows.
  37. \b
  38. ''', re.VERBOSE)
  39. # This is a hack until something more reasonable is figured out
  40. specialCaseParser = re.compile(r'file\((\d+)\)')
  41. sinceEpochParser = re.compile(r'\d+ .+ since epoch')
  42. # The weird OR is there to always have at least empty string for group 1
  43. tapTestNumParser = re.compile(r'^((?:not ok)|(?:ok)|(?:warning)|(?:info)) (\d+) -')
  44. if len(sys.argv) == 2:
  45. cmdPath = sys.argv[1]
  46. else:
  47. cmdPath = os.path.join(catchPath, scriptCommon.getBuildExecutable())
  48. overallResult = 0
  49. def diffFiles(fileA, fileB):
  50. with io.open(fileA, 'r', encoding='utf-8', errors='surrogateescape') as file:
  51. aLines = [line.rstrip() for line in file.readlines()]
  52. with io.open(fileB, 'r', encoding='utf-8', errors='surrogateescape') as file:
  53. bLines = [line.rstrip() for line in file.readlines()]
  54. shortenedFilenameA = fileA.rsplit(os.sep, 1)[-1]
  55. shortenedFilenameB = fileB.rsplit(os.sep, 1)[-1]
  56. diff = difflib.unified_diff(aLines, bLines, fromfile=shortenedFilenameA, tofile=shortenedFilenameB, n=0)
  57. return [line for line in diff if line[0] in ('+', '-')]
  58. def normalizeFilepath(line):
  59. # Sometimes the path separators used by compiler and Python can differ,
  60. # so we try to match the path with both forward and backward path
  61. # separators, to make the paths relative to Catch2 repo root.
  62. forwardSlashPath = catchPath.replace('\\', '/')
  63. if forwardSlashPath in line:
  64. line = line.replace(forwardSlashPath + '/', '')
  65. backwardSlashPath = catchPath.replace('/', '\\')
  66. if backwardSlashPath in line:
  67. line = line.replace(backwardSlashPath + '\\', '')
  68. m = langFilenameParser.match(line)
  69. if m:
  70. filepath = m.group(0)
  71. # go from \ in windows paths to /
  72. filepath = filepath.replace('\\', '/')
  73. # remove start of relative path
  74. filepath = filepath.replace('../', '')
  75. line = line[:m.start()] + filepath + line[m.end():]
  76. return line
  77. def filterLine(line, isCompact):
  78. line = normalizeFilepath(line)
  79. # strip source line numbers
  80. m = filelocParser.match(line)
  81. if m:
  82. # note that this also strips directories, leaving only the filename
  83. filename, lnum = m.groups()
  84. lnum = ":<line number>" if lnum else ""
  85. line = filename + lnum + line[m.end():]
  86. else:
  87. line = lineNumberParser.sub(" ", line)
  88. if isCompact:
  89. line = line.replace(': FAILED', ': failed')
  90. line = line.replace(': PASSED', ': passed')
  91. # strip out the test order number in TAP to avoid massive diffs for every change
  92. line = tapTestNumParser.sub("\g<1> {test-number} -", line)
  93. # strip Catch2 version number
  94. line = versionParser.sub("<version>", line)
  95. # replace *null* with 0
  96. line = nullParser.sub("0", line)
  97. # strip executable name
  98. line = exeNameParser.sub("<exe-name>", line)
  99. # strip hexadecimal numbers (presumably pointer values)
  100. line = hexParser.sub("0x<hex digits>", line)
  101. # strip durations and timestamps
  102. line = junitDurationsParser.sub(' time="{duration}"', line)
  103. line = durationParser.sub(' duration="{duration}"', line)
  104. line = timestampsParser.sub('{iso8601-timestamp}', line)
  105. line = specialCaseParser.sub('file:\g<1>', line)
  106. line = sinceEpochParser.sub('{since-epoch-report}', line)
  107. return line
  108. def get_rawResultsPath(baseName):
  109. return os.path.join(rootPath, '_{0}.tmp'.format(baseName))
  110. def get_baselinesPath(baseName):
  111. return os.path.join(rootPath, '{0}.approved.txt'.format(baseName))
  112. def get_filteredResultsPath(baseName):
  113. return os.path.join(rootPath, '{0}.unapproved.txt'.format(baseName))
  114. def run_test(baseName, args):
  115. args[0:0] = [cmdPath]
  116. if not os.path.exists(cmdPath):
  117. raise Exception("Executable doesn't exist at " + cmdPath)
  118. print(args)
  119. rawResultsPath = get_rawResultsPath(baseName)
  120. f = open(rawResultsPath, 'w')
  121. subprocess.call(args, stdout=f, stderr=f)
  122. f.close()
  123. def check_outputs(baseName):
  124. global overallResult
  125. rawResultsPath = get_rawResultsPath(baseName)
  126. baselinesPath = get_baselinesPath(baseName)
  127. filteredResultsPath = get_filteredResultsPath(baseName)
  128. rawFile = io.open(rawResultsPath, 'r', encoding='utf-8', errors='surrogateescape')
  129. filteredFile = io.open(filteredResultsPath, 'w', encoding='utf-8', errors='surrogateescape')
  130. for line in rawFile:
  131. filteredFile.write(filterLine(line, 'compact' in baseName).rstrip() + "\n")
  132. filteredFile.close()
  133. rawFile.close()
  134. os.remove(rawResultsPath)
  135. print()
  136. print(baseName + ":")
  137. if os.path.exists(baselinesPath):
  138. diffResult = diffFiles(baselinesPath, filteredResultsPath)
  139. if diffResult:
  140. print('\n'.join(diffResult))
  141. print(" \n****************************\n \033[91mResults differed")
  142. if len(diffResult) > overallResult:
  143. overallResult = len(diffResult)
  144. else:
  145. os.remove(filteredResultsPath)
  146. print(" \033[92mResults matched")
  147. print("\033[0m")
  148. else:
  149. print(" first approval")
  150. if overallResult == 0:
  151. overallResult = 1
  152. def approve(baseName, args):
  153. run_test(baseName, args)
  154. check_outputs(baseName)
  155. print("Running approvals against executable:")
  156. print(" " + cmdPath)
  157. base_args = ["--order", "lex", "--rng-seed", "1", "--colour-mode", "none"]
  158. ## special cases first:
  159. # Standard console reporter
  160. approve("console.std", ["~[!nonportable]~[!benchmark]~[approvals] *"] + base_args)
  161. # console reporter, include passes, warn about No Assertions, limit failures to first 4
  162. approve("console.swa4", ["~[!nonportable]~[!benchmark]~[approvals] *", "-s", "-w", "NoAssertions", "-x", "4"] + base_args)
  163. ## Common reporter checks: include passes, warn about No Assertions
  164. reporters = ('console', 'junit', 'xml', 'compact', 'sonarqube', 'tap', 'teamcity', 'automake')
  165. for reporter in reporters:
  166. filename = '{}.sw'.format(reporter)
  167. common_args = ["~[!nonportable]~[!benchmark]~[approvals] *", "-s", "-w", "NoAssertions"] + base_args
  168. reporter_args = ['-r', reporter]
  169. approve(filename, common_args + reporter_args)
  170. ## All reporters at the same time
  171. common_args = ["~[!nonportable]~[!benchmark]~[approvals] *", "-s", "-w", "NoAssertions"] + base_args
  172. filenames = ['{}.sw.multi'.format(reporter) for reporter in reporters]
  173. reporter_args = []
  174. for reporter, filename in zip(reporters, filenames):
  175. reporter_args += ['-r', '{}::out={}'.format(reporter, get_rawResultsPath(filename))]
  176. run_test("default.sw.multi", common_args + reporter_args)
  177. check_outputs("default.sw.multi")
  178. for reporter, filename in zip(reporters, filenames):
  179. check_outputs(filename)
  180. if overallResult != 0:
  181. print("If these differences are expected, run approve.py to approve new baselines.")
  182. exit(overallResult)