Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # Copyright (C) 2013 Intel Corporation |
| 2 | # |
| 3 | # Released under the MIT license (see COPYING.MIT) |
| 4 | |
| 5 | # Main unittest module used by testimage.bbclass |
| 6 | # This provides the oeRuntimeTest base class which is inherited by all tests in meta/lib/oeqa/runtime. |
| 7 | |
| 8 | # It also has some helper functions and it's responsible for actually starting the tests |
| 9 | |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 10 | import os, re, mmap, sys |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 11 | import unittest |
| 12 | import inspect |
| 13 | import subprocess |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 14 | import signal |
Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 15 | try: |
| 16 | import bb |
| 17 | except ImportError: |
| 18 | pass |
| 19 | import logging |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 20 | |
| 21 | import oeqa.runtime |
| 22 | # Exported test doesn't require sdkext |
| 23 | try: |
| 24 | import oeqa.sdkext |
| 25 | except ImportError: |
| 26 | pass |
Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 27 | from oeqa.utils.decorators import LogResults, gettag, getResults |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 28 | from oeqa.utils import avoid_paths_in_environ |
Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 29 | |
| 30 | logger = logging.getLogger("BitBake") |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 31 | |
| 32 | def getVar(obj): |
| 33 | #extend form dict, if a variable didn't exists, need find it in testcase |
| 34 | class VarDict(dict): |
| 35 | def __getitem__(self, key): |
| 36 | return gettag(obj, key) |
| 37 | return VarDict() |
| 38 | |
| 39 | def checkTags(tc, tagexp): |
| 40 | return eval(tagexp, None, getVar(tc)) |
| 41 | |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 42 | def filterByTagExp(testsuite, tagexp): |
| 43 | if not tagexp: |
| 44 | return testsuite |
| 45 | caseList = [] |
| 46 | for each in testsuite: |
| 47 | if not isinstance(each, unittest.BaseTestSuite): |
| 48 | if checkTags(each, tagexp): |
| 49 | caseList.append(each) |
| 50 | else: |
| 51 | caseList.append(filterByTagExp(each, tagexp)) |
| 52 | return testsuite.__class__(caseList) |
| 53 | |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 54 | @LogResults |
| 55 | class oeTest(unittest.TestCase): |
| 56 | |
| 57 | longMessage = True |
| 58 | |
| 59 | @classmethod |
| 60 | def hasPackage(self, pkg): |
| 61 | for item in oeTest.tc.pkgmanifest.split('\n'): |
| 62 | if re.match(pkg, item): |
| 63 | return True |
| 64 | return False |
| 65 | |
| 66 | @classmethod |
| 67 | def hasFeature(self,feature): |
| 68 | |
| 69 | if feature in oeTest.tc.imagefeatures or \ |
| 70 | feature in oeTest.tc.distrofeatures: |
| 71 | return True |
| 72 | else: |
| 73 | return False |
| 74 | |
| 75 | class oeRuntimeTest(oeTest): |
| 76 | def __init__(self, methodName='runTest'): |
| 77 | self.target = oeRuntimeTest.tc.target |
| 78 | super(oeRuntimeTest, self).__init__(methodName) |
| 79 | |
| 80 | def setUp(self): |
| 81 | # Check if test needs to run |
| 82 | if self.tc.sigterm: |
| 83 | self.fail("Got SIGTERM") |
| 84 | elif (type(self.target).__name__ == "QemuTarget"): |
| 85 | self.assertTrue(self.target.check(), msg = "Qemu not running?") |
| 86 | |
Patrick Williams | d7e9631 | 2015-09-22 08:09:05 -0500 | [diff] [blame] | 87 | self.setUpLocal() |
| 88 | |
| 89 | # a setup method before tests but after the class instantiation |
| 90 | def setUpLocal(self): |
| 91 | pass |
| 92 | |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 93 | def tearDown(self): |
Patrick Williams | f1e5d69 | 2016-03-30 15:21:19 -0500 | [diff] [blame] | 94 | res = getResults() |
| 95 | # If a test fails or there is an exception dump |
| 96 | # for QemuTarget only |
| 97 | if (type(self.target).__name__ == "QemuTarget" and |
| 98 | (self.id() in res.getErrorList() or |
| 99 | self.id() in res.getFailList())): |
| 100 | self.tc.host_dumper.create_dir(self._testMethodName) |
| 101 | self.tc.host_dumper.dump_host() |
| 102 | self.target.target_dumper.dump_target( |
| 103 | self.tc.host_dumper.dump_dir) |
| 104 | print ("%s dump data stored in %s" % (self._testMethodName, |
| 105 | self.tc.host_dumper.dump_dir)) |
| 106 | |
| 107 | self.tearDownLocal() |
| 108 | |
| 109 | # Method to be run after tearDown and implemented by child classes |
| 110 | def tearDownLocal(self): |
| 111 | pass |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 112 | |
| 113 | #TODO: use package_manager.py to install packages on any type of image |
| 114 | def install_packages(self, packagelist): |
| 115 | for package in packagelist: |
| 116 | (status, result) = self.target.run("smart install -y "+package) |
| 117 | if status != 0: |
| 118 | return status |
| 119 | |
| 120 | class oeSDKTest(oeTest): |
| 121 | def __init__(self, methodName='runTest'): |
| 122 | self.sdktestdir = oeSDKTest.tc.sdktestdir |
| 123 | super(oeSDKTest, self).__init__(methodName) |
| 124 | |
| 125 | @classmethod |
| 126 | def hasHostPackage(self, pkg): |
| 127 | |
| 128 | if re.search(pkg, oeTest.tc.hostpkgmanifest): |
| 129 | return True |
| 130 | return False |
| 131 | |
| 132 | def _run(self, cmd): |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 133 | return subprocess.check_output(". %s > /dev/null; %s;" % (self.tc.sdkenv, cmd), shell=True) |
| 134 | |
| 135 | class oeSDKExtTest(oeSDKTest): |
| 136 | def _run(self, cmd): |
| 137 | # extensible sdk shows a warning if found bitbake in the path |
| 138 | # because can cause contamination, i.e. use devtool from |
| 139 | # poky/scripts instead of eSDK one. |
| 140 | env = os.environ.copy() |
| 141 | paths_to_avoid = ['bitbake/bin', 'poky/scripts'] |
| 142 | env['PATH'] = avoid_paths_in_environ(paths_to_avoid) |
| 143 | |
| 144 | return subprocess.check_output(". %s > /dev/null;"\ |
| 145 | " %s;" % (self.tc.sdkenv, cmd), shell=True, env=env) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 146 | |
| 147 | def getmodule(pos=2): |
| 148 | # stack returns a list of tuples containg frame information |
| 149 | # First element of the list the is current frame, caller is 1 |
| 150 | frameinfo = inspect.stack()[pos] |
| 151 | modname = inspect.getmodulename(frameinfo[1]) |
| 152 | #modname = inspect.getmodule(frameinfo[0]).__name__ |
| 153 | return modname |
| 154 | |
| 155 | def skipModule(reason, pos=2): |
| 156 | modname = getmodule(pos) |
| 157 | if modname not in oeTest.tc.testsrequired: |
| 158 | raise unittest.SkipTest("%s: %s" % (modname, reason)) |
| 159 | else: |
| 160 | raise Exception("\nTest %s wants to be skipped.\nReason is: %s" \ |
| 161 | "\nTest was required in TEST_SUITES, so either the condition for skipping is wrong" \ |
| 162 | "\nor the image really doesn't have the required feature/package when it should." % (modname, reason)) |
| 163 | |
| 164 | def skipModuleIf(cond, reason): |
| 165 | |
| 166 | if cond: |
| 167 | skipModule(reason, 3) |
| 168 | |
| 169 | def skipModuleUnless(cond, reason): |
| 170 | |
| 171 | if not cond: |
| 172 | skipModule(reason, 3) |
Patrick Williams | d8c66bc | 2016-06-20 12:57:21 -0500 | [diff] [blame] | 173 | |
| 174 | _buffer_logger = "" |
| 175 | def custom_verbose(msg, *args, **kwargs): |
| 176 | global _buffer_logger |
| 177 | if msg[-1] != "\n": |
| 178 | _buffer_logger += msg |
| 179 | else: |
| 180 | _buffer_logger += msg |
| 181 | try: |
| 182 | bb.plain(_buffer_logger.rstrip("\n"), *args, **kwargs) |
| 183 | except NameError: |
| 184 | logger.info(_buffer_logger.rstrip("\n"), *args, **kwargs) |
| 185 | _buffer_logger = "" |
| 186 | |
| 187 | class TestContext(object): |
| 188 | def __init__(self, d): |
| 189 | self.d = d |
| 190 | |
| 191 | self.testsuites = self._get_test_suites() |
| 192 | self.testslist = self._get_tests_list(d.getVar("BBPATH", True).split(':')) |
| 193 | self.testsrequired = self._get_test_suites_required() |
| 194 | |
| 195 | self.filesdir = os.path.join(os.path.dirname(os.path.abspath( |
| 196 | oeqa.runtime.__file__)), "files") |
| 197 | self.imagefeatures = d.getVar("IMAGE_FEATURES", True).split() |
| 198 | self.distrofeatures = d.getVar("DISTRO_FEATURES", True).split() |
| 199 | |
| 200 | # get testcase list from specified file |
| 201 | # if path is a relative path, then relative to build/conf/ |
| 202 | def _read_testlist(self, fpath, builddir): |
| 203 | if not os.path.isabs(fpath): |
| 204 | fpath = os.path.join(builddir, "conf", fpath) |
| 205 | if not os.path.exists(fpath): |
| 206 | bb.fatal("No such manifest file: ", fpath) |
| 207 | tcs = [] |
| 208 | for line in open(fpath).readlines(): |
| 209 | line = line.strip() |
| 210 | if line and not line.startswith("#"): |
| 211 | tcs.append(line) |
| 212 | return " ".join(tcs) |
| 213 | |
| 214 | # return test list by type also filter if TEST_SUITES is specified |
| 215 | def _get_tests_list(self, bbpath): |
| 216 | testslist = [] |
| 217 | |
| 218 | type = self._get_test_namespace() |
| 219 | |
| 220 | # This relies on lib/ under each directory in BBPATH being added to sys.path |
| 221 | # (as done by default in base.bbclass) |
| 222 | for testname in self.testsuites: |
| 223 | if testname != "auto": |
| 224 | if testname.startswith("oeqa."): |
| 225 | testslist.append(testname) |
| 226 | continue |
| 227 | found = False |
| 228 | for p in bbpath: |
| 229 | if os.path.exists(os.path.join(p, 'lib', 'oeqa', type, testname + '.py')): |
| 230 | testslist.append("oeqa." + type + "." + testname) |
| 231 | found = True |
| 232 | break |
| 233 | elif os.path.exists(os.path.join(p, 'lib', 'oeqa', type, testname.split(".")[0] + '.py')): |
| 234 | testslist.append("oeqa." + type + "." + testname) |
| 235 | found = True |
| 236 | break |
| 237 | if not found: |
| 238 | bb.fatal('Test %s specified in TEST_SUITES could not be found in lib/oeqa/runtime under BBPATH' % testname) |
| 239 | |
| 240 | if "auto" in self.testsuites: |
| 241 | def add_auto_list(path): |
| 242 | if not os.path.exists(os.path.join(path, '__init__.py')): |
| 243 | bb.fatal('Tests directory %s exists but is missing __init__.py' % path) |
| 244 | files = sorted([f for f in os.listdir(path) if f.endswith('.py') and not f.startswith('_')]) |
| 245 | for f in files: |
| 246 | module = 'oeqa.' + type + '.' + f[:-3] |
| 247 | if module not in testslist: |
| 248 | testslist.append(module) |
| 249 | |
| 250 | for p in bbpath: |
| 251 | testpath = os.path.join(p, 'lib', 'oeqa', type) |
| 252 | bb.debug(2, 'Searching for tests in %s' % testpath) |
| 253 | if os.path.exists(testpath): |
| 254 | add_auto_list(testpath) |
| 255 | |
| 256 | return testslist |
| 257 | |
| 258 | def loadTests(self): |
| 259 | setattr(oeTest, "tc", self) |
| 260 | |
| 261 | testloader = unittest.TestLoader() |
| 262 | testloader.sortTestMethodsUsing = None |
| 263 | suites = [testloader.loadTestsFromName(name) for name in self.testslist] |
| 264 | suites = filterByTagExp(suites, getattr(self, "tagexp", None)) |
| 265 | |
| 266 | def getTests(test): |
| 267 | '''Return all individual tests executed when running the suite.''' |
| 268 | # Unfortunately unittest does not have an API for this, so we have |
| 269 | # to rely on implementation details. This only needs to work |
| 270 | # for TestSuite containing TestCase. |
| 271 | method = getattr(test, '_testMethodName', None) |
| 272 | if method: |
| 273 | # leaf case: a TestCase |
| 274 | yield test |
| 275 | else: |
| 276 | # Look into TestSuite. |
| 277 | tests = getattr(test, '_tests', []) |
| 278 | for t1 in tests: |
| 279 | for t2 in getTests(t1): |
| 280 | yield t2 |
| 281 | |
| 282 | # Determine dependencies between suites by looking for @skipUnlessPassed |
| 283 | # method annotations. Suite A depends on suite B if any method in A |
| 284 | # depends on a method on B. |
| 285 | for suite in suites: |
| 286 | suite.dependencies = [] |
| 287 | suite.depth = 0 |
| 288 | for test in getTests(suite): |
| 289 | methodname = getattr(test, '_testMethodName', None) |
| 290 | if methodname: |
| 291 | method = getattr(test, methodname) |
| 292 | depends_on = getattr(method, '_depends_on', None) |
| 293 | if depends_on: |
| 294 | for dep_suite in suites: |
| 295 | if depends_on in [getattr(t, '_testMethodName', None) for t in getTests(dep_suite)]: |
| 296 | if dep_suite not in suite.dependencies and \ |
| 297 | dep_suite is not suite: |
| 298 | suite.dependencies.append(dep_suite) |
| 299 | break |
| 300 | else: |
| 301 | logger.warning("Test %s was declared as @skipUnlessPassed('%s') but that test is either not defined or not active. Will run the test anyway." % |
| 302 | (test, depends_on)) |
| 303 | |
| 304 | # Use brute-force topological sort to determine ordering. Sort by |
| 305 | # depth (higher depth = must run later), with original ordering to |
| 306 | # break ties. |
| 307 | def set_suite_depth(suite): |
| 308 | for dep in suite.dependencies: |
| 309 | new_depth = set_suite_depth(dep) + 1 |
| 310 | if new_depth > suite.depth: |
| 311 | suite.depth = new_depth |
| 312 | return suite.depth |
| 313 | |
| 314 | for index, suite in enumerate(suites): |
| 315 | set_suite_depth(suite) |
| 316 | suite.index = index |
| 317 | suites.sort(cmp=lambda a,b: cmp((a.depth, a.index), (b.depth, b.index))) |
| 318 | |
| 319 | self.suite = testloader.suiteClass(suites) |
| 320 | |
| 321 | return self.suite |
| 322 | |
| 323 | def runTests(self): |
| 324 | logger.info("Test modules %s" % self.testslist) |
| 325 | if hasattr(self, "tagexp") and self.tagexp: |
| 326 | logger.info("Filter test cases by tags: %s" % self.tagexp) |
| 327 | logger.info("Found %s tests" % self.suite.countTestCases()) |
| 328 | runner = unittest.TextTestRunner(verbosity=2) |
| 329 | if 'bb' in sys.modules: |
| 330 | runner.stream.write = custom_verbose |
| 331 | |
| 332 | return runner.run(self.suite) |
| 333 | |
| 334 | class ImageTestContext(TestContext): |
| 335 | def __init__(self, d, target, host_dumper): |
| 336 | super(ImageTestContext, self).__init__(d) |
| 337 | |
| 338 | self.tagexp = d.getVar("TEST_SUITES_TAGS", True) |
| 339 | |
| 340 | self.target = target |
| 341 | self.host_dumper = host_dumper |
| 342 | |
| 343 | manifest = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), |
| 344 | d.getVar("IMAGE_LINK_NAME", True) + ".manifest") |
| 345 | nomanifest = d.getVar("IMAGE_NO_MANIFEST", True) |
| 346 | if nomanifest is None or nomanifest != "1": |
| 347 | try: |
| 348 | with open(manifest) as f: |
| 349 | self.pkgmanifest = f.read() |
| 350 | except IOError as e: |
| 351 | bb.fatal("No package manifest file found. Did you build the image?\n%s" % e) |
| 352 | else: |
| 353 | self.pkgmanifest = "" |
| 354 | |
| 355 | self.sigterm = False |
| 356 | self.origsigtermhandler = signal.getsignal(signal.SIGTERM) |
| 357 | signal.signal(signal.SIGTERM, self._sigterm_exception) |
| 358 | |
| 359 | def _sigterm_exception(self, signum, stackframe): |
| 360 | bb.warn("TestImage received SIGTERM, shutting down...") |
| 361 | self.sigterm = True |
| 362 | self.target.stop() |
| 363 | |
| 364 | def _get_test_namespace(self): |
| 365 | return "runtime" |
| 366 | |
| 367 | def _get_test_suites(self): |
| 368 | testsuites = [] |
| 369 | |
| 370 | manifests = (self.d.getVar("TEST_SUITES_MANIFEST", True) or '').split() |
| 371 | if manifests: |
| 372 | for manifest in manifests: |
| 373 | testsuites.extend(self._read_testlist(manifest, |
| 374 | self.d.getVar("TOPDIR", True)).split()) |
| 375 | |
| 376 | else: |
| 377 | testsuites = self.d.getVar("TEST_SUITES", True).split() |
| 378 | |
| 379 | return testsuites |
| 380 | |
| 381 | def _get_test_suites_required(self): |
| 382 | return [t for t in self.d.getVar("TEST_SUITES", True).split() if t != "auto"] |
| 383 | |
| 384 | def loadTests(self): |
| 385 | super(ImageTestContext, self).loadTests() |
| 386 | setattr(oeRuntimeTest, "pscmd", "ps -ef" if oeTest.hasPackage("procps") else "ps") |
| 387 | |
| 388 | class SDKTestContext(TestContext): |
| 389 | def __init__(self, d, sdktestdir, sdkenv, tcname, *args): |
| 390 | super(SDKTestContext, self).__init__(d) |
| 391 | |
| 392 | self.sdktestdir = sdktestdir |
| 393 | self.sdkenv = sdkenv |
| 394 | self.tcname = tcname |
| 395 | |
| 396 | if not hasattr(self, 'target_manifest'): |
| 397 | self.target_manifest = d.getVar("SDK_TARGET_MANIFEST", True) |
| 398 | try: |
| 399 | with open(self.target_manifest) as f: |
| 400 | self.pkgmanifest = f.read() |
| 401 | except IOError as e: |
| 402 | bb.fatal("No package manifest file found. Did you build the sdk image?\n%s" % e) |
| 403 | |
| 404 | if not hasattr(self, 'host_manifest'): |
| 405 | self.host_manifest = d.getVar("SDK_HOST_MANIFEST", True) |
| 406 | try: |
| 407 | with open(self.host_manifest) as f: |
| 408 | self.hostpkgmanifest = f.read() |
| 409 | except IOError as e: |
| 410 | bb.fatal("No host package manifest file found. Did you build the sdk image?\n%s" % e) |
| 411 | |
| 412 | def _get_test_namespace(self): |
| 413 | return "sdk" |
| 414 | |
| 415 | def _get_test_suites(self): |
| 416 | return (self.d.getVar("TEST_SUITES_SDK", True) or "auto").split() |
| 417 | |
| 418 | def _get_test_suites_required(self): |
| 419 | return [t for t in (self.d.getVar("TEST_SUITES_SDK", True) or \ |
| 420 | "auto").split() if t != "auto"] |
| 421 | |
| 422 | class SDKExtTestContext(SDKTestContext): |
| 423 | def __init__(self, d, sdktestdir, sdkenv, tcname, *args): |
| 424 | self.target_manifest = d.getVar("SDK_EXT_TARGET_MANIFEST", True) |
| 425 | self.host_manifest = d.getVar("SDK_EXT_HOST_MANIFEST", True) |
| 426 | if args: |
| 427 | self.cm = args[0] # Compatibility mode for run SDK tests |
| 428 | else: |
| 429 | self.cm = False |
| 430 | |
| 431 | super(SDKExtTestContext, self).__init__(d, sdktestdir, sdkenv, tcname) |
| 432 | |
| 433 | self.sdkextfilesdir = os.path.join(os.path.dirname(os.path.abspath( |
| 434 | oeqa.sdkext.__file__)), "files") |
| 435 | |
| 436 | def _get_test_namespace(self): |
| 437 | if self.cm: |
| 438 | return "sdk" |
| 439 | else: |
| 440 | return "sdkext" |
| 441 | |
| 442 | def _get_test_suites(self): |
| 443 | return (self.d.getVar("TEST_SUITES_SDK_EXT", True) or "auto").split() |
| 444 | |
| 445 | def _get_test_suites_required(self): |
| 446 | return [t for t in (self.d.getVar("TEST_SUITES_SDK_EXT", True) or \ |
| 447 | "auto").split() if t != "auto"] |