TestCenter Reference
CodeTest.py
Go to the documentation of this file.
1 #
2 # Copyright 2016, MeVis Medical Solutions AG
3 #
4 # The user may use this file in accordance with the license agreement provided with
5 # the Software or, alternatively, in accordance with the terms contained in a
6 # written agreement between the user and MeVis Medical Solutions AG.
7 #
8 # For further information use the contact form at https://www.mevislab.de/contact
9 #
10 
11 
13 
14 from TestSupport.Macros import EXPECT_EQ, EXPECT_TRUE, ASSERT_TRUE
15 from TestSupport.Logging import infoHTML, errorHTML
16 from mevis import MLAB, MLABPackageManager, MLABTestCaseDatabase
17 import os
18 import os.path
19 import sys
20 import re
21 import json
22 
23 # Allows to inject test functions for a compiled GoogleTest executable into a FunctionalTestCase
24 # Example (to be put into a TestCase's python file:
25 # from TestSupport import CodeTest
26 # CodeTest.inject("$(MLAB_MeVisLab_Standard)/CodeTests/bin/MLBaseTest", ctx)
27 def inject(executable, context):
28  # replace package variable with the path to the binary directory in this case:
29  # (the default would be the source directory, which might not be the same for some setups)
30  match = re.search(r"\$\‍(MLAB_([^_)]+)_([^_)]+)\‍)", executable)
31  if match:
32  package = MLABPackageManager.packageByIdentifier(match.group(1) + "/" + match.group(2))
33  if package:
34  executable = executable.replace(match.group(0), package.binariesPath())
35  test = CodeTest(executable = context.expandFilename(executable))
36  test.injectTestFunctions(context)
37 
38 pathToBuildSystemScripts = MLABPackageManager.getPathInPackage('MeVis/BuildSystem', 'BuildTools/Scripts')
39 if os.path.isdir(pathToBuildSystemScripts):
40  if not pathToBuildSystemScripts in sys.path:
41  sys.path.insert(0, pathToBuildSystemScripts)
42  from MeVisPython import MeVisPython
43 del pathToBuildSystemScripts
44 
45 def _parseGoogleTestOutput(result, allowRestart=False):
46  colorOk = 'color: rgb(78,154,6);'
47  colorFailed = 'color: rgb(204,0,0);'
48  commonStyle = 'font-family: monospace;'
49 
50  statusFormat = '<div style="%s"><span style="%s">%%s</span>%%s</div>'
51  errorFormat = statusFormat % (commonStyle, colorFailed)
52  infoFormat = statusFormat % (commonStyle, colorOk)
53 
54  statusRegExp = re.compile("(?P<status>\\[\\s*\\S+\\s*\\])(?P<message>.*)")
55 
56  hasNotifiedStdErrOutput = False
57 
58  # Check for concrete error or failure information in stderr message. If yes then
59  # really take it as an error. Often we have normal stderr prints of suppressed
60  # leaks from sanitizer or stupidly other informational but non-failure-messages,
61  # which should not be handled as error. If they do not actually print
62  # something like "error:" or "failed:" they should be tolerated.
63  lowerStderrMsg = result['stderr'].lower()
64  stdErrContainsError = (lowerStderrMsg.find("error") >=0) or (lowerStderrMsg.find("failed") >=0)
65 
66  needsRestartWithoutSanitizer = False
67  if stdErrContainsError:
68  # Check whether test failed with a sanitizer failure caused by a sanitizer bug. It makes a
69  # kind of unit test incompatible.
70  if lowerStderrMsg.find("sanitizer check failed:") >=0 and \
71  lowerStderrMsg.find("src/libsanitizer/asan/asan_malloc_linux.cc") >= 0 and \
72  lowerStderrMsg.find("allocated_for_dlsym") >= 0:
73  needsRestartWithoutSanitizer = allowRestart
74 
75  if not needsRestartWithoutSanitizer:
76 
77  EXPECT_EQ(result["status"], 0, msg='The test status is not 0.', logOnSuccess=False)
78  EXPECT_TRUE(result["ok"], msg='The test run failed.', logOnSuccess=False)
79  ASSERT_TRUE("stdout" in result, msg='The test output does not exists.', logOnSuccess=False)
80 
81  for outChannel in ['stdout', 'stderr']:
82  for line in result[outChannel].splitlines():
83  # Before first std::err message print a notice that there are also std::cerr messages.
84  if outChannel=='stderr' and not hasNotifiedStdErrOutput:
85  if stdErrContainsError:
86  errorHTML(('Note: The stderr channel output contained "error" or "failed" which is handled as test failure.'))
87  infoHTML('The test printed the following messages to stderr channel:')
88  hasNotifiedStdErrOutput = True
89 
90  m = statusRegExp.match(line)
91  if m:
92  status = m.group('status').replace(' ', '&nbsp;').replace('\t', '&nbsp;'*4)
93  message = m.group('message').replace(' ', '&nbsp;').replace('\t', '&nbsp;'*4)
94  if 'FAILED' in status:
95  errorHTML(errorFormat % (status, message))
96  else:
97  infoHTML(infoFormat % (status, message))
98  else:
99  line = line.replace(' ', '&nbsp;').replace('\t', '&nbsp;'*4)
100  infoHTML(('<div style="%s">' + line + '</div>') % commonStyle)
101 
102  return needsRestartWithoutSanitizer
103 
105  def __init__(self, name, sourceName):
106  self.namename = name
107  self.sourceNamesourceName = sourceName
108  self.__testFunctions__testFunctions = []
109  def addFunction(self, function):
110  self.__testFunctions__testFunctions.append(function)
111  def getFunctions(self):
112  return self.__testFunctions__testFunctions
113 
114 
116  def __init__(self, name, sourceName):
117  self.namename = name
118  self.sourceNamesourceName = sourceName
119 
120 
121 class CodeTest:
122  # Either proFile or executable need to be passed in. Executable is without debug and "
123  def __init__(self, cmakeFile = None, executable = None):
124  self.__testFunctionGroups__testFunctionGroups = {}
125  self.__infoDict__infoDict = None
126  self.__uniqueFunctionNames__uniqueFunctionNames = None
127  self.cmakeFilecmakeFile = cmakeFile
128  self.executableexecutable = executable
129  self.__searchedExecutable__searchedExecutable = False
130 
131  if cmakeFile is not None:
132  self.namename = self._parseCMakeFile_parseCMakeFile(cmakeFile)
133 
134  def _getUniqueTestCaseName(self, name):
135  uniqueName = name
136  i = 0
137  while MLABTestCaseDatabase.testCaseInfo(uniqueName):
138  if i == 0:
139  # first append _CodeTest, and later also a number
140  name += '_CodeTest'
141  uniqueName = name
142  else:
143  uniqueName = name + str(i)
144  i += 1
145  return uniqueName
146 
147  def getInfoDict(self):
148  if self.__infoDict__infoDict == None:
149  package = MLABPackageManager.findPackageContainingPath(self.cmakeFilecmakeFile)
150  self.__infoDict__infoDict = {'package': package.packageIdentifier(),
151  'author': '',
152  'file': self.cmakeFilecmakeFile,
153  'lineno': 1,
154  'type': 'CodeTestTestCase',
155  'scriptFile': 'ScriptFileNotAvailable'}
156  return self.__infoDict__infoDict
157 
158  def getTestFunctionGroups(self): return self.__testFunctionGroups
159 
160  def getExecutable(self):
161  if self.executableexecutable is None and not self.__searchedExecutable__searchedExecutable:
162  self.__searchedExecutable__searchedExecutable = True
163  self.executableexecutable = self._findExecutables_findExecutables()
164  if not self.__executableExists__executableExists(self.executableexecutable):
165  MLAB.logError("Could not locate CodeTest executable: " + self.namename)
166  else:
167  if not os.path.exists(self.executableexecutable):
168  # turn base filename into something existing
169  exeSuffix = ".exe" if MLAB.isWindows() else ""
170  if os.path.exists(self.executableexecutable + exeSuffix):
171  self.executableexecutable += exeSuffix
172  # fall back to debug executable if release executable does not exist:
173  elif os.path.exists(self.executableexecutable + "_d" + exeSuffix):
174  self.executableexecutable += "_d" + exeSuffix
175  return self.executableexecutable
176 
177  @staticmethod
178  def __executableExists(executable):
179  return executable and os.path.exists(executable)
180 
181  def existsExecutable(self):
182  return self.__executableExists__executableExists(self.getExecutablegetExecutable())
183 
184  def buildProject(self):
185  if self.existsExecutableexistsExecutable():
186  os.remove(self.executableexecutable)
187  executable = 'cmake'
188  arguments = ['--build', '.', '--config', ('Debug' if MLAB.isDebug() else 'Release'), '--target', self.namename]
189  result = MLAB.runCommandStdInOut(executable, arguments, self._getBuildRoot_getBuildRoot())
190  # force to search for the executable again:
191  self.executableexecutable = None
192  self.__searchedExecutable__searchedExecutable = False
193  if not self.existsExecutableexistsExecutable():
194  MLAB.showCritical(self._getError_getError(result))
195 
196  if result['ok'] != True:
197  MLAB.logError(self._getError_getError(result))
198  return result['ok'] == True
199 
200  def _getError(self, result):
201  message = []
202  stderr = result.get('stderr')
203  stdout = result.get('stdout')
204  if stdout: message.append(stdout.encode('latin1').decode('UTF-8'))
205  if stderr: message.append(stderr.encode('latin1').decode('UTF-8'))
206  if not stdout and not stderr:
207  message.append('An unknown error occurred')
208  return '\n'.join(message)
209 
210  def _parseCMakeFile(self, cmakeFile):
211  testRegex = re.compile(r'^(?!#)\s*mlab_add_test\‍(\s*(\w*)')
212  target = None
213  with open(cmakeFile, 'r') as f:
214  for line in f:
215  match = testRegex.match(line)
216  if match and match.group(1):
217  target = match.group(1)
218  return target
219  return target
220 
221  def _getBuildRoot(self):
222  if 'MLAB_BUILD' in os.environ:
223  buildRoot = os.environ['MLAB_BUILD']
224  else:
225  buildRoot = os.path.normpath(os.path.join(MLABPackageManager.packageByIdentifier('MeVisLab/IDE').binariesPath(), "..", ".."))
226  return buildRoot
227 
228  def _findExecutables(self):
229  executable = None
230  command = 'ctest'
231  testArguments = ["-V", "-R", self.namename, "-C", ("Debug" if MLAB.isDebug() else "Release"), "--show-only=json-v1"]
232  result = MLAB.runCommandStdInOut(command, testArguments, self._getBuildRoot_getBuildRoot())
233  if result['ok'] == True:
234  testDetails = json.loads(result['stdout'])
235  if 'tests' in testDetails and len(testDetails['tests'])>0 and 'command' in testDetails['tests'][0] and len(testDetails['tests'][0]['command']) > 0:
236  executable = testDetails['tests'][0]['command'][0]
237  return executable
238 
239  def loadTestFunctions(self):
240  if not self.existsExecutableexistsExecutable():
241  self.buildProjectbuildProject()
242  if self.existsExecutableexistsExecutable():
243  executable = self.executableexecutable
244  result = MLAB.runCommandStdInOut(executable, ["--gtest_list_tests"], os.path.dirname(executable))
245  if result['ok'] == True:
246  self._parseTestFunctionsFromOutput_parseTestFunctionsFromOutput(result['stdout'])
247 
248  def __getUniqueFunctionName(self, name):
249  i = 0
250  uniqueName = name
251  while uniqueName in self.__uniqueFunctionNames__uniqueFunctionNames:
252  i += 1
253  uniqueName = name + str(i)
254  self.__uniqueFunctionNames__uniqueFunctionNames.add(uniqueName)
255  return uniqueName
256 
257  def _parseTestFunctionsFromOutput(self, output):
258  identifierRegExp = re.compile(r'^[\S]+$')
259  self.__testFunctionGroups__testFunctionGroups = []
260  self.__uniqueFunctionNames__uniqueFunctionNames = set()
261  group = None
262  groupIdx = 1
263  functionIdx = 1
264  for line in output.splitlines():
265  commentIdx = line.find('#')
266  if commentIdx >= 0:
267  line = line[:commentIdx].rstrip(' ')
268  if not line.startswith(' '):
269  name = line.rstrip('.')
270  if identifierRegExp.match(name):
271  sourceName = 'GROUP%03d_%s' % (groupIdx, name.replace('/', '_'))
272  group = TestFunctionGroup(name, sourceName)
273  self.__testFunctionGroups__testFunctionGroups.append(group)
274  groupIdx += 1
275  else:
276  name = line.strip()
277  if identifierRegExp.match(name):
278  isDisabled = False
279  uniqueName = name
280  if uniqueName.startswith('DISABLED_'):
281  uniqueName = uniqueName[len('DISABLED_'):]
282  isDisabled = True
283  uniqueName = self.__getUniqueFunctionName__getUniqueFunctionName(uniqueName)
284  sourceName = 'TEST%03d_%s' % (functionIdx, uniqueName.replace('/', '_'))
285  if isDisabled:
286  sourceName = 'DISABLED_' + sourceName
287  function = TestFunction(name, sourceName)
288  group.addFunction(function)
289  functionIdx += 1
290 
291  def injectTestFunctions(self, context):
292  self.loadTestFunctionsloadTestFunctions()
293 
294  testCode = ''
295  ctx = context
296 
297  globalSymbols = globals()
298  localSymbols= locals()
299 
300  for group in self.getTestFunctionGroupsgetTestFunctionGroups():
301  for function in group.getFunctions():
302  testCode += self._functionCode_functionCode(group, function)
303  testCode += self._groupCode_groupCode(group)
304 
305  eval(compile(testCode, '<string>', 'exec'), globalSymbols, localSymbols)
306 
307  def _groupCode(self, group):
308  functions = []
309  for f in group.getFunctions():
310  functions.append("'" + f.sourceName + "'")
311  return '''def %(sourceName)s():
312  return %(functions)s,
313 ctx.setScriptVariable(%(sourceName)s.__name__, %(sourceName)s)
314 ''' % dict(sourceName = group.sourceName, functions = ",".join(functions))
315 
316  def _hasSanitizerSuppression(self, sanitizerPrefix, envVar, compilerVersion):
317  mlabRootPath = MLAB.variable('MLAB_MeVis_BuildSystem')
318  suppressionFile = mlabRootPath + '/BuildTools/linux/' + sanitizerPrefix + u'SanitizerSuppressions_gcc' + compilerVersion + '.supp'
319  #print(suppressionFile)
320  return os.path.isfile(suppressionFile) and os.path.exists(suppressionFile)
321 
322  def _determineSanitizerSetUpCode(self):
323  if MLAB.isLinux():
324  compilerInfo = MLAB.compilerInfo()
325  compilerVersion = '4'
326  if compilerInfo.find(u'GCC_64_7') >= 0:
327  compilerVersion = u'7'
328  elif compilerInfo.find(u'GCC_64_9') >= 0:
329  compilerVersion = u'9'
330  elif compilerInfo.find(u'') >= 0:
331  compilerVersion = u'5'
332 
333  hasSuppressions = self._hasSanitizerSuppression_hasSanitizerSuppression('Leak', 'LSAN_OPTIONS', compilerVersion) and \
334  self._hasSanitizerSuppression_hasSanitizerSuppression('Address', 'ASAN_OPTIONS', compilerVersion) and \
335  self._hasSanitizerSuppression_hasSanitizerSuppression('UB', 'UBSAN_OPTIONS', compilerVersion) and \
336  self._hasSanitizerSuppression_hasSanitizerSuppression('Thread', 'TSAN_OPTIONS', compilerVersion)
337  if hasSuppressions:
338  return '''
339  MLAB.log(u"Running in sanitizer environment of gcc%(compilerVer)s ('asan' and 'ubsan' libs with LD_PRELOAD as well as suppression files).")
340  mlabRootPath = MLAB.variable("MLAB_MeVis_BuildSystem")
341  sanitizerEnvVarIntro = "suppressions=" + mlabRootPath + os.sep
342  os.environ["LSAN_OPTIONS"] = sanitizerEnvVarIntro + u"BuildTools/linux/LeakSanitizerSuppressions_gcc%(compilerVer)s.supp"
343  os.environ["ASAN_OPTIONS"] = sanitizerEnvVarIntro + u"BuildTools/linux/AddressSanitizerSuppressions_gcc%(compilerVer)s.supp"
344  os.environ["UBSAN_OPTIONS"] = sanitizerEnvVarIntro + u"BuildTools/linux/UBSanitizerSuppressions_gcc%(compilerVer)s.supp"
345  os.environ["TSAN_OPTIONS"] = sanitizerEnvVarIntro + u"linux/ThreadSanitizerSuppressions_gcc%(compilerVer)s.supp"
346  os.environ["LD_PRELOAD"] = u"/usr/lib/gcc/x86_64-linux-gnu/%(compilerVer)s/libasan.so:/usr/lib/gcc/x86_64-linux-gnu/%(compilerVer)s/libubsan.so"
347  ''' % dict(compilerVer = compilerVersion)
348  else:
349  return '''
350  MLAB.log(u"Could not find (all) sanitizer suppression files: not running test in gcc sanitizer environment.")
351  '''
352  else:
353  return ''
354 
355  def _functionCode(self, group, function):
356  exePath = self.getExecutablegetExecutable()
357  exePath = exePath.replace('\\', '\\\\')
358  saniCode = self._determineSanitizerSetUpCode_determineSanitizerSetUpCode()
359  return '''def %(sourceName)s():
360  executable = "%(executable)s"
361  for package in MLABPackageManager.packages():
362  os.environ[package.packageEnvVariableName()] = package.scriptsPath()
363 %(sanitizerSetUpCode)s
364  result = MLAB.runCommandStdInOut(executable, ['--gtest_also_run_disabled_tests', '--gtest_filter=%(groupName)s.%(functionName)s'], os.path.dirname(executable))
365  from TestSupport.CodeTest import _parseGoogleTestOutput
366  needsRestartWithoutSanitizer = _parseGoogleTestOutput(result, True)
367  if needsRestartWithoutSanitizer:
368  os.environ["LD_PRELOAD"] = ""
369  MLAB.log(u"Sanitizer is incompatible with unittest. Restarting test without sanitzer preload.")
370  result = MLAB.runCommandStdInOut(executable, ['--gtest_also_run_disabled_tests', '--gtest_filter=%(groupName)s.%(functionName)s'], os.path.dirname(executable))
371  _parseGoogleTestOutput(result)
372 ctx.setScriptVariable(%(sourceName)s.__name__, %(sourceName)s)
373 ''' % dict(sourceName = function.sourceName, executable = exePath, sanitizerSetUpCode = saniCode, groupName = group.name, functionName = function.name)
374 
375 
def __init__(self, cmakeFile=None, executable=None)
Definition: CodeTest.py:123
def _parseCMakeFile(self, cmakeFile)
Definition: CodeTest.py:210
def __getUniqueFunctionName(self, name)
Definition: CodeTest.py:248
def _getError(self, result)
Definition: CodeTest.py:200
def _determineSanitizerSetUpCode(self)
Definition: CodeTest.py:322
def __executableExists(executable)
Definition: CodeTest.py:178
def _groupCode(self, group)
Definition: CodeTest.py:307
def _hasSanitizerSuppression(self, sanitizerPrefix, envVar, compilerVersion)
Definition: CodeTest.py:316
def _parseTestFunctionsFromOutput(self, output)
Definition: CodeTest.py:257
def _functionCode(self, group, function)
Definition: CodeTest.py:355
def injectTestFunctions(self, context)
Definition: CodeTest.py:291
def __init__(self, name, sourceName)
Definition: CodeTest.py:105
def __init__(self, name, sourceName)
Definition: CodeTest.py:116
def inject(executable, context)
Definition: CodeTest.py:27
Package to provide logging functions.
Definition: Logging.py:1
def errorHTML(message, type='', depth=0)
Put an error to the log while interpreting HTML.
Definition: Logging.py:132
def infoHTML(message, type='', depth=0)
Put an info message to the log while interpreting HTML.
Definition: Logging.py:89
Adds GoogleTest like methods.
Definition: Macros.py:1
def EXPECT_EQ(expected, actual, msg=None, logOnSuccess=None)
Expect given values to be equal.
Definition: Macros.py:129
def EXPECT_TRUE(expr, msg=None, logOnSuccess=None)
Expect given expression to evaluate to true.
Definition: Macros.py:81
def ASSERT_TRUE(expr, msg=None, logOnSuccess=None)
Throw exception if given expression does not evaluate to true.
Definition: Macros.py:69