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