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