# # Copyright (C) 2013 Intel Corporation # # SPDX-License-Identifier: MIT # # Main unittest module used by testimage.bbclass # This provides the oeRuntimeTest base class which is inherited by all tests in meta/lib/oeqa/runtime. # It also has some helper functions and it's responsible for actually starting the tests import os, re, sys import unittest import inspect import subprocess import signal import shutil import functools try: import bb except ImportError: pass import logging import oeqa.runtime # Exported test doesn't require sdkext try: import oeqa.sdkext except ImportError: pass from oeqa.utils.decorators import LogResults, gettag, getResults logger = logging.getLogger("BitBake") def getVar(obj): #extend form dict, if a variable didn't exists, need find it in testcase class VarDict(dict): def __getitem__(self, key): return gettag(obj, key) return VarDict() def checkTags(tc, tagexp): return eval(tagexp, None, getVar(tc)) def filterByTagExp(testsuite, tagexp): if not tagexp: return testsuite caseList = [] for each in testsuite: if not isinstance(each, unittest.BaseTestSuite): if checkTags(each, tagexp): caseList.append(each) else: caseList.append(filterByTagExp(each, tagexp)) return testsuite.__class__(caseList) @LogResults class oeTest(unittest.TestCase): pscmd = "ps" longMessage = True @classmethod def hasPackage(self, pkg): """ True if the full package name exists in the manifest, False otherwise. """ return pkg in oeTest.tc.pkgmanifest @classmethod def hasPackageMatch(self, match): """ True if match exists in the manifest as a regular expression substring, False otherwise. """ for s in oeTest.tc.pkgmanifest: if re.match(match, s): return True return False @classmethod def hasFeature(self,feature): if feature in oeTest.tc.imagefeatures or \ feature in oeTest.tc.distrofeatures: return True else: return False class oeRuntimeTest(oeTest): def __init__(self, methodName='runTest'): self.target = oeRuntimeTest.tc.target super(oeRuntimeTest, self).__init__(methodName) def setUp(self): # Install packages in the DUT self.tc.install_uninstall_packages(self.id()) # Check if test needs to run if self.tc.sigterm: self.fail("Got SIGTERM") elif (type(self.target).__name__ == "QemuTarget"): self.assertTrue(self.target.check(), msg = "Qemu not running?") self.setUpLocal() # a setup method before tests but after the class instantiation def setUpLocal(self): pass def tearDown(self): # Uninstall packages in the DUT self.tc.install_uninstall_packages(self.id(), False) res = getResults() # If a test fails or there is an exception dump # for QemuTarget only if (type(self.target).__name__ == "QemuTarget" and (self.id() in res.getErrorList() or self.id() in res.getFailList())): self.tc.host_dumper.create_dir(self._testMethodName) self.tc.host_dumper.dump_host() self.target.target_dumper.dump_target( self.tc.host_dumper.dump_dir) print ("%s dump data stored in %s" % (self._testMethodName, self.tc.host_dumper.dump_dir)) self.tearDownLocal() # Method to be run after tearDown and implemented by child classes def tearDownLocal(self): pass def getmodule(pos=2): # stack returns a list of tuples containg frame information # First element of the list the is current frame, caller is 1 frameinfo = inspect.stack()[pos] modname = inspect.getmodulename(frameinfo[1]) #modname = inspect.getmodule(frameinfo[0]).__name__ return modname def skipModule(reason, pos=2): modname = getmodule(pos) if modname not in oeTest.tc.testsrequired: raise unittest.SkipTest("%s: %s" % (modname, reason)) else: raise Exception("\nTest %s wants to be skipped.\nReason is: %s" \ "\nTest was required in TEST_SUITES, so either the condition for skipping is wrong" \ "\nor the image really doesn't have the required feature/package when it should." % (modname, reason)) def skipModuleIf(cond, reason): if cond: skipModule(reason, 3) def skipModuleUnless(cond, reason): if not cond: skipModule(reason, 3) _buffer_logger = "" def custom_verbose(msg, *args, **kwargs): global _buffer_logger if msg[-1] != "\n": _buffer_logger += msg else: _buffer_logger += msg try: bb.plain(_buffer_logger.rstrip("\n"), *args, **kwargs) except NameError: logger.info(_buffer_logger.rstrip("\n"), *args, **kwargs) _buffer_logger = "" class TestContext(object): def __init__(self, d, exported=False): self.d = d self.testsuites = self._get_test_suites() if exported: path = [os.path.dirname(os.path.abspath(__file__))] extrapath = "" else: path = d.getVar("BBPATH").split(':') extrapath = "lib/oeqa" self.testslist = self._get_tests_list(path, extrapath) self.testsrequired = self._get_test_suites_required() self.filesdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "runtime/files") self.corefilesdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files") self.imagefeatures = d.getVar("IMAGE_FEATURES").split() self.distrofeatures = d.getVar("DISTRO_FEATURES").split() # get testcase list from specified file # if path is a relative path, then relative to build/conf/ def _read_testlist(self, fpath, builddir): if not os.path.isabs(fpath): fpath = os.path.join(builddir, "conf", fpath) if not os.path.exists(fpath): bb.fatal("No such manifest file: ", fpath) tcs = [] for line in open(fpath).readlines(): line = line.strip() if line and not line.startswith("#"): tcs.append(line) return " ".join(tcs) # return test list by type also filter if TEST_SUITES is specified def _get_tests_list(self, bbpath, extrapath): testslist = [] type = self._get_test_namespace() # This relies on lib/ under each directory in BBPATH being added to sys.path # (as done by default in base.bbclass) for testname in self.testsuites: if testname != "auto": if testname.startswith("oeqa."): testslist.append(testname) continue found = False for p in bbpath: if os.path.exists(os.path.join(p, extrapath, type, testname + ".py")): testslist.append("oeqa." + type + "." + testname) found = True break elif os.path.exists(os.path.join(p, extrapath, type, testname.split(".")[0] + ".py")): testslist.append("oeqa." + type + "." + testname) found = True break if not found: bb.fatal('Test %s specified in TEST_SUITES could not be found in lib/oeqa/runtime under BBPATH' % testname) if "auto" in self.testsuites: def add_auto_list(path): files = sorted([f for f in os.listdir(path) if f.endswith('.py') and not f.startswith('_')]) for f in files: module = 'oeqa.' + type + '.' + f[:-3] if module not in testslist: testslist.append(module) for p in bbpath: testpath = os.path.join(p, 'lib', 'oeqa', type) bb.debug(2, 'Searching for tests in %s' % testpath) if os.path.exists(testpath): add_auto_list(testpath) return testslist def getTestModules(self): """ Returns all the test modules in the testlist. """ import pkgutil modules = [] for test in self.testslist: if re.search("\w+\.\w+\.test_\S+", test): test = '.'.join(t.split('.')[:3]) module = pkgutil.get_loader(test) modules.append(module) return modules def getModulefromID(self, test_id): """ Returns the test module based on a test id. """ module_name = ".".join(test_id.split(".")[:3]) modules = self.getTestModules() for module in modules: if module.name == module_name: return module return None def getTests(self, test): '''Return all individual tests executed when running the suite.''' # Unfortunately unittest does not have an API for this, so we have # to rely on implementation details. This only needs to work # for TestSuite containing TestCase. method = getattr(test, '_testMethodName', None) if method: # leaf case: a TestCase yield test else: # Look into TestSuite. tests = getattr(test, '_tests', []) for t1 in tests: for t2 in self.getTests(t1): yield t2 def loadTests(self): setattr(oeTest, "tc", self) testloader = unittest.TestLoader() testloader.sortTestMethodsUsing = None suites = [testloader.loadTestsFromName(name) for name in self.testslist] suites = filterByTagExp(suites, getattr(self, "tagexp", None)) # Determine dependencies between suites by looking for @skipUnlessPassed # method annotations. Suite A depends on suite B if any method in A # depends on a method on B. for suite in suites: suite.dependencies = [] suite.depth = 0 for test in self.getTests(suite): methodname = getattr(test, '_testMethodName', None) if methodname: method = getattr(test, methodname) depends_on = getattr(method, '_depends_on', None) if depends_on: for dep_suite in suites: if depends_on in [getattr(t, '_testMethodName', None) for t in self.getTests(dep_suite)]: if dep_suite not in suite.dependencies and \ dep_suite is not suite: suite.dependencies.append(dep_suite) break else: logger.warning("Test %s was declared as @skipUnlessPassed('%s') but that test is either not defined or not active. Will run the test anyway." % (test, depends_on)) # Use brute-force topological sort to determine ordering. Sort by # depth (higher depth = must run later), with original ordering to # break ties. def set_suite_depth(suite): for dep in suite.dependencies: new_depth = set_suite_depth(dep) + 1 if new_depth > suite.depth: suite.depth = new_depth return suite.depth for index, suite in enumerate(suites): set_suite_depth(suite) suite.index = index def cmp(a, b): return (a > b) - (a < b) def cmpfunc(a, b): return cmp((a.depth, a.index), (b.depth, b.index)) suites.sort(key=functools.cmp_to_key(cmpfunc)) self.suite = testloader.suiteClass(suites) return self.suite def runTests(self): logger.info("Test modules %s" % self.testslist) if hasattr(self, "tagexp") and self.tagexp: logger.info("Filter test cases by tags: %s" % self.tagexp) logger.info("Found %s tests" % self.suite.countTestCases()) runner = unittest.TextTestRunner(verbosity=2) if 'bb' in sys.modules: runner.stream.write = custom_verbose return runner.run(self.suite) class RuntimeTestContext(TestContext): def __init__(self, d, target, exported=False): super(RuntimeTestContext, self).__init__(d, exported) self.target = target self.pkgmanifest = {} manifest = os.path.join(d.getVar("DEPLOY_DIR_IMAGE"), d.getVar("IMAGE_LINK_NAME") + ".manifest") nomanifest = d.getVar("IMAGE_NO_MANIFEST") if nomanifest is None or nomanifest != "1": try: with open(manifest) as f: for line in f: (pkg, arch, version) = line.strip().split() self.pkgmanifest[pkg] = (version, arch) except IOError as e: bb.fatal("No package manifest file found. Did you build the image?\n%s" % e) def _get_test_namespace(self): return "runtime" def _get_test_suites(self): testsuites = [] manifests = (self.d.getVar("TEST_SUITES_MANIFEST") or '').split() if manifests: for manifest in manifests: testsuites.extend(self._read_testlist(manifest, self.d.getVar("TOPDIR")).split()) else: testsuites = self.d.getVar("TEST_SUITES").split() return testsuites def _get_test_suites_required(self): return [t for t in self.d.getVar("TEST_SUITES").split() if t != "auto"] def loadTests(self): super(RuntimeTestContext, self).loadTests() if oeTest.hasPackage("procps"): oeRuntimeTest.pscmd = "ps -ef" def extract_packages(self): """ Find packages that will be needed during runtime. """ modules = self.getTestModules() bbpaths = self.d.getVar("BBPATH").split(":") shutil.rmtree(self.d.getVar("TEST_EXTRACTED_DIR")) shutil.rmtree(self.d.getVar("TEST_PACKAGED_DIR")) for module in modules: json_file = self._getJsonFile(module) if json_file: needed_packages = self._getNeededPackages(json_file) self._perform_package_extraction(needed_packages) def _perform_package_extraction(self, needed_packages): """ Extract packages that will be needed during runtime. """ import oe.path extracted_path = self.d.getVar("TEST_EXTRACTED_DIR") packaged_path = self.d.getVar("TEST_PACKAGED_DIR") for key,value in needed_packages.items(): packages = () if isinstance(value, dict): packages = (value, ) elif isinstance(value, list): packages = value else: bb.fatal("Failed to process needed packages for %s; " "Value must be a dict or list" % key) for package in packages: pkg = package["pkg"] rm = package.get("rm", False) extract = package.get("extract", True) if extract: dst_dir = os.path.join(extracted_path, pkg) else: dst_dir = os.path.join(packaged_path) # Extract package and copy it to TEST_EXTRACTED_DIR pkg_dir = self._extract_in_tmpdir(pkg) if extract: # Same package used for more than one test, # don't need to extract again. if os.path.exists(dst_dir): continue oe.path.copytree(pkg_dir, dst_dir) shutil.rmtree(pkg_dir) # Copy package to TEST_PACKAGED_DIR else: self._copy_package(pkg) def _getJsonFile(self, module): """ Returns the path of the JSON file for a module, empty if doesn't exitst. """ module_file = module.path json_file = "%s.json" % module_file.rsplit(".", 1)[0] if os.path.isfile(module_file) and os.path.isfile(json_file): return json_file else: return "" def _getNeededPackages(self, json_file, test=None): """ Returns a dict with needed packages based on a JSON file. If a test is specified it will return the dict just for that test. """ import json needed_packages = {} with open(json_file) as f: test_packages = json.load(f) for key,value in test_packages.items(): needed_packages[key] = value if test: if test in needed_packages: needed_packages = needed_packages[test] else: needed_packages = {} return needed_packages def _extract_in_tmpdir(self, pkg): """" Returns path to a temp directory where the package was extracted without dependencies. """ from oeqa.utils.package_manager import get_package_manager pkg_path = os.path.join(self.d.getVar("TEST_INSTALL_TMP_DIR"), pkg) pm = get_package_manager(self.d, pkg_path) extract_dir = pm.extract(pkg) shutil.rmtree(pkg_path) return extract_dir def _copy_package(self, pkg): """ Copy the RPM, DEB or IPK package to dst_dir """ from oeqa.utils.package_manager import get_package_manager pkg_path = os.path.join(self.d.getVar("TEST_INSTALL_TMP_DIR"), pkg) dst_dir = self.d.getVar("TEST_PACKAGED_DIR") pm = get_package_manager(self.d, pkg_path) pkg_info = pm.package_info(pkg) file_path = pkg_info[pkg]["filepath"] shutil.copy2(file_path, dst_dir) shutil.rmtree(pkg_path) def install_uninstall_packages(self, test_id, pkg_dir, install): """ Check if the test requires a package and Install/Uninstall it in the DUT """ test = test_id.split(".")[4] module = self.getModulefromID(test_id) json = self._getJsonFile(module) if json: needed_packages = self._getNeededPackages(json, test) if needed_packages: self._install_uninstall_packages(needed_packages, pkg_dir, install) def _install_uninstall_packages(self, needed_packages, pkg_dir, install=True): """ Install/Uninstall packages in the DUT without using a package manager """ if isinstance(needed_packages, dict): packages = [needed_packages] elif isinstance(needed_packages, list): packages = needed_packages for package in packages: pkg = package["pkg"] rm = package.get("rm", False) extract = package.get("extract", True) src_dir = os.path.join(pkg_dir, pkg) # Install package if install and extract: self.target.connection.copy_dir_to(src_dir, "/") # Uninstall package elif not install and rm: self.target.connection.delete_dir_structure(src_dir, "/") class ImageTestContext(RuntimeTestContext): def __init__(self, d, target, host_dumper): super(ImageTestContext, self).__init__(d, target) self.tagexp = d.getVar("TEST_SUITES_TAGS") self.host_dumper = host_dumper self.sigterm = False self.origsigtermhandler = signal.getsignal(signal.SIGTERM) signal.signal(signal.SIGTERM, self._sigterm_exception) def _sigterm_exception(self, signum, stackframe): bb.warn("TestImage received SIGTERM, shutting down...") self.sigterm = True self.target.stop() def install_uninstall_packages(self, test_id, install=True): """ Check if the test requires a package and Install/Uninstall it in the DUT """ pkg_dir = self.d.getVar("TEST_EXTRACTED_DIR") super(ImageTestContext, self).install_uninstall_packages(test_id, pkg_dir, install) class ExportTestContext(RuntimeTestContext): def __init__(self, d, target, exported=False, parsedArgs={}): """ This class is used when exporting tests and when are executed outside OE environment. parsedArgs can contain the following: - tag: Filter test by tag. """ super(ExportTestContext, self).__init__(d, target, exported) tag = parsedArgs.get("tag", None) self.tagexp = tag if tag != None else d.getVar("TEST_SUITES_TAGS") self.sigterm = None def install_uninstall_packages(self, test_id, install=True): """ Check if the test requires a package and Install/Uninstall it in the DUT """ export_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) extracted_dir = self.d.getVar("TEST_EXPORT_EXTRACTED_DIR") pkg_dir = os.path.join(export_dir, extracted_dir) super(ExportTestContext, self).install_uninstall_packages(test_id, pkg_dir, install)