diff options
Diffstat (limited to 'meta/lib/oeqa/core')
31 files changed, 784 insertions, 408 deletions
diff --git a/meta/lib/oeqa/core/case.py b/meta/lib/oeqa/core/case.py index 917a2aa3f8..bc4446a938 100644 --- a/meta/lib/oeqa/core/case.py +++ b/meta/lib/oeqa/core/case.py @@ -1,6 +1,11 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# +import base64 +import zlib import unittest from oeqa.core.exception import OEQAMissingVariable @@ -29,6 +34,8 @@ class OETestCase(unittest.TestCase): @classmethod def _oeSetUpClass(clss): _validate_td_vars(clss.td, clss.td_vars, "class") + if hasattr(clss, 'setUpHooker') and callable(getattr(clss, 'setUpHooker')): + clss.setUpHooker() clss.setUpClassMethod() @classmethod @@ -36,11 +43,63 @@ class OETestCase(unittest.TestCase): clss.tearDownClassMethod() def _oeSetUp(self): - for d in self.decorators: - d.setUpDecorator() + try: + for d in self.decorators: + d.setUpDecorator() + except: + for d in self.decorators: + d.tearDownDecorator() + raise self.setUpMethod() def _oeTearDown(self): for d in self.decorators: d.tearDownDecorator() self.tearDownMethod() + +class OEPTestResultTestCase: + """ + Mix-in class to provide functions to make interacting with extraresults for + the purposes of storing ptestresult data. + """ + @staticmethod + def _compress_log(log): + logdata = log.encode("utf-8") if isinstance(log, str) else log + logdata = zlib.compress(logdata) + logdata = base64.b64encode(logdata).decode("utf-8") + return {"compressed" : logdata} + + def ptest_rawlog(self, log): + if not hasattr(self, "extraresults"): + self.extraresults = {"ptestresult.sections" : {}} + self.extraresults["ptestresult.rawlogs"] = {"log" : self._compress_log(log)} + + def ptest_section(self, section, duration = None, log = None, logfile = None, exitcode = None): + if not hasattr(self, "extraresults"): + self.extraresults = {"ptestresult.sections" : {}} + + sections = self.extraresults.get("ptestresult.sections") + if section not in sections: + sections[section] = {} + + if log is not None: + sections[section]["log"] = self._compress_log(log) + elif logfile is not None: + with open(logfile, "rb") as f: + sections[section]["log"] = self._compress_log(f.read()) + + if duration is not None: + sections[section]["duration"] = duration + if exitcode is not None: + sections[section]["exitcode"] = exitcode + + def ptest_result(self, section, test, result): + if not hasattr(self, "extraresults"): + self.extraresults = {"ptestresult.sections" : {}} + + sections = self.extraresults.get("ptestresult.sections") + if section not in sections: + sections[section] = {} + resultname = "ptestresult.{}.{}".format(section, test) + self.extraresults[resultname] = {"status" : result} + diff --git a/meta/lib/oeqa/core/cases/example/test_basic.py b/meta/lib/oeqa/core/cases/example/test_basic.py index 11cf3800cc..d77edcdcec 100644 --- a/meta/lib/oeqa/core/cases/example/test_basic.py +++ b/meta/lib/oeqa/core/cases/example/test_basic.py @@ -1,5 +1,7 @@ # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.case import OETestCase from oeqa.core.decorator.depends import OETestDepends diff --git a/meta/lib/oeqa/core/context.py b/meta/lib/oeqa/core/context.py index 821aec8836..2abe353d27 100644 --- a/meta/lib/oeqa/core/context.py +++ b/meta/lib/oeqa/core/context.py @@ -1,5 +1,7 @@ -# Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +## Copyright (C) 2016 Intel Corporation +# +# SPDX-License-Identifier: MIT +# import os import sys @@ -7,6 +9,7 @@ import json import time import logging import collections +import unittest from oeqa.core.loader import OETestLoader from oeqa.core.runner import OETestRunner @@ -28,6 +31,9 @@ class OETestContext(object): self._registry = {} self._registry['cases'] = collections.OrderedDict() + self.results = unittest.TestResult() + unittest.registerResult(self.results) + def _read_modules_from_manifest(self, manifest): if not os.path.exists(manifest): raise OEQAMissingManifest("Manifest does not exist on %s" % manifest) @@ -43,20 +49,35 @@ class OETestContext(object): def skipTests(self, skips): if not skips: return + def skipfuncgen(skipmsg): + def func(): + raise unittest.SkipTest(skipmsg) + return func + class_ids = {} for test in self.suites: + if test.__class__ not in class_ids: + class_ids[test.__class__] = '.'.join(test.id().split('.')[:-1]) + for skip in skips: + if (test.id()+'.').startswith(skip+'.'): + setattr(test, 'setUp', skipfuncgen('Skip by the command line argument "%s"' % skip)) + for tclass in class_ids: + cid = class_ids[tclass] for skip in skips: - if test.id().startswith(skip): - setattr(test, 'setUp', lambda: test.skipTest('Skip by the command line argument "%s"' % skip)) + if (cid + '.').startswith(skip + '.'): + setattr(tclass, 'setUpHooker', skipfuncgen('Skip by the command line argument "%s"' % skip)) def loadTests(self, module_paths, modules=[], tests=[], - modules_manifest="", modules_required=[], filters={}): + modules_manifest="", modules_required=[], **kwargs): if modules_manifest: modules = self._read_modules_from_manifest(modules_manifest) self.loader = self.loaderClass(self, module_paths, modules, tests, - modules_required, filters) + modules_required, **kwargs) self.suites = self.loader.discover() + def prepareSuite(self, suites, processes): + return suites + def runTests(self, processes=None, skips=[]): self.runner = self.runnerClass(self, descriptions=False, verbosity=2) @@ -64,14 +85,10 @@ class OETestContext(object): self.skipTests(skips) self._run_start_time = time.time() - if processes: - from oeqa.core.utils.concurrencytest import ConcurrentTestSuite - - concurrent_suite = ConcurrentTestSuite(self.suites, processes) - result = self.runner.run(concurrent_suite) - else: + self._run_end_time = self._run_start_time + if not processes: self.runner.buffer = True - result = self.runner.run(self.suites) + result = self.runner.run(self.prepareSuite(self.suites, processes)) self._run_end_time = time.time() return result @@ -87,22 +104,27 @@ class OETestContextExecutor(object): name = 'core' help = 'core test component example' description = 'executes core test suite example' + datetime = time.strftime("%Y%m%d%H%M%S") default_cases = [os.path.join(os.path.abspath(os.path.dirname(__file__)), 'cases/example')] default_test_data = os.path.join(default_cases[0], 'data.json') default_tests = None + default_json_result_dir = None def register_commands(self, logger, subparsers): self.parser = subparsers.add_parser(self.name, help=self.help, description=self.description, group='components') - self.default_output_log = '%s-results-%s.log' % (self.name, - time.strftime("%Y%m%d%H%M%S")) + self.default_output_log = '%s-results-%s.log' % (self.name, self.datetime) self.parser.add_argument('--output-log', action='store', default=self.default_output_log, help="results output log, default: %s" % self.default_output_log) + self.parser.add_argument('--json-result-dir', action='store', + default=self.default_json_result_dir, + help="json result output dir, default: %s" % self.default_json_result_dir) + group = self.parser.add_mutually_exclusive_group() group.add_argument('--run-tests', action='store', nargs='+', default=self.default_tests, @@ -138,6 +160,8 @@ class OETestContextExecutor(object): fh = logging.FileHandler(args.output_log) fh.setFormatter(formatter) logger.addHandler(fh) + if getattr(args, 'verbose', False): + logger.setLevel('DEBUG') return logger @@ -165,6 +189,22 @@ class OETestContextExecutor(object): self.module_paths = args.CASES_PATHS + def _get_json_result_dir(self, args): + return args.json_result_dir + + def _get_configuration(self): + td = self.tc_kwargs['init']['td'] + configuration = {'TEST_TYPE': self.name, + 'MACHINE': td.get("MACHINE"), + 'DISTRO': td.get("DISTRO"), + 'IMAGE_BASENAME': td.get("IMAGE_BASENAME"), + 'DATETIME': td.get("DATETIME")} + return configuration + + def _get_result_id(self, configuration): + return '%s_%s_%s_%s' % (configuration['TEST_TYPE'], configuration['IMAGE_BASENAME'], + configuration['MACHINE'], self.datetime) + def _pre_run(self): pass @@ -183,7 +223,16 @@ class OETestContextExecutor(object): else: self._pre_run() rc = self.tc.runTests(**self.tc_kwargs['run']) - rc.logDetails() + + json_result_dir = self._get_json_result_dir(args) + if json_result_dir: + configuration = self._get_configuration() + rc.logDetails(json_result_dir, + configuration, + self._get_result_id(configuration)) + else: + rc.logDetails() + rc.logSummary(self.name) output_link = os.path.join(os.path.dirname(args.output_log), diff --git a/meta/lib/oeqa/core/decorator/__init__.py b/meta/lib/oeqa/core/decorator/__init__.py index 14d7bfcd35..1a82518ab6 100644 --- a/meta/lib/oeqa/core/decorator/__init__.py +++ b/meta/lib/oeqa/core/decorator/__init__.py @@ -1,8 +1,12 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from functools import wraps from abc import abstractmethod, ABCMeta +from oeqa.core.utils.misc import strToList decoratorClasses = set() @@ -60,12 +64,16 @@ class OETestDiscover(OETestDecorator): def discover(registry): return registry['cases'] -class OETestFilter(OETestDecorator): +def OETestTag(*tags): + expandedtags = [] + for tag in tags: + expandedtags += strToList(tag) + def decorator(item): + if hasattr(item, "__oeqa_testtags"): + # do not append, create a new list (to handle classes with inheritance) + item.__oeqa_testtags = list(item.__oeqa_testtags) + expandedtags + else: + item.__oeqa_testtags = expandedtags + return item + return decorator - # OETestLoader call it while loading the tests - # in loadTestsFromTestCase method, it needs to - # return a bool, True if needs to be filtered. - # This method must consume the filter used. - @abstractmethod - def filtrate(self, filters): - return False diff --git a/meta/lib/oeqa/core/decorator/data.py b/meta/lib/oeqa/core/decorator/data.py index f0f65abb39..bc4939e87c 100644 --- a/meta/lib/oeqa/core/decorator/data.py +++ b/meta/lib/oeqa/core/decorator/data.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.exception import OEQAMissingVariable @@ -15,6 +18,26 @@ def has_feature(td, feature): return True return False +def has_machine(td, machine): + """ + Checks for MACHINE. + """ + + if (machine in td.get('MACHINE', '')): + return True + return False + +def is_qemu(td, qemu): + """ + Checks if MACHINE is qemu. + """ + + machine = td.get('MACHINE', '') + if (qemu in td.get('MACHINE', '') or + machine.startswith('qemu')): + return True + return False + @registerDecorator class skipIfDataVar(OETestDecorator): """ @@ -110,3 +133,90 @@ class skipIfNotFeature(OETestDecorator): self.logger.debug(msg) if not has_feature(self.case.td, self.value): self.case.skipTest(self.msg) + +@registerDecorator +class skipIfFeature(OETestDecorator): + """ + Skip test based on DISTRO_FEATURES. + + value must not be in distro features or it will skip the test + with msg as the reason. + """ + + attrs = ('value', 'msg') + + def setUpDecorator(self): + msg = ('Checking if %s is not in DISTRO_FEATURES ' + 'or IMAGE_FEATURES' % (self.value)) + self.logger.debug(msg) + if has_feature(self.case.td, self.value): + self.case.skipTest(self.msg) + +@registerDecorator +class skipIfNotMachine(OETestDecorator): + """ + Skip test based on MACHINE. + + value must be match MACHINE or it will skip the test + with msg as the reason. + """ + + attrs = ('value', 'msg') + + def setUpDecorator(self): + msg = ('Checking if %s is not this MACHINE' % self.value) + self.logger.debug(msg) + if not has_machine(self.case.td, self.value): + self.case.skipTest(self.msg) + +@registerDecorator +class skipIfMachine(OETestDecorator): + """ + Skip test based on Machine. + + value must not be this machine or it will skip the test + with msg as the reason. + """ + + attrs = ('value', 'msg') + + def setUpDecorator(self): + msg = ('Checking if %s is this MACHINE' % self.value) + self.logger.debug(msg) + if has_machine(self.case.td, self.value): + self.case.skipTest(self.msg) + +@registerDecorator +class skipIfNotQemu(OETestDecorator): + """ + Skip test based on MACHINE. + + value must be a qemu MACHINE or it will skip the test + with msg as the reason. + """ + + attrs = ('value', 'msg') + + def setUpDecorator(self): + msg = ('Checking if %s is not this MACHINE' % self.value) + self.logger.debug(msg) + if not is_qemu(self.case.td, self.value): + self.case.skipTest(self.msg) + +@registerDecorator +class skipIfQemu(OETestDecorator): + """ + Skip test based on Qemu Machine. + + value must not be a qemu machine or it will skip the test + with msg as the reason. + """ + + attrs = ('value', 'msg') + + def setUpDecorator(self): + msg = ('Checking if %s is this MACHINE' % self.value) + self.logger.debug(msg) + if is_qemu(self.case.td, self.value): + self.case.skipTest(self.msg) + diff --git a/meta/lib/oeqa/core/decorator/depends.py b/meta/lib/oeqa/core/decorator/depends.py index 950dbaa67a..33f0841cab 100644 --- a/meta/lib/oeqa/core/decorator/depends.py +++ b/meta/lib/oeqa/core/decorator/depends.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from unittest import SkipTest diff --git a/meta/lib/oeqa/core/decorator/oeid.py b/meta/lib/oeqa/core/decorator/oeid.py deleted file mode 100644 index ea8017a55a..0000000000 --- a/meta/lib/oeqa/core/decorator/oeid.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -from . import OETestFilter, registerDecorator -from oeqa.core.utils.misc import intToList - -def _idFilter(oeid, filters): - return False if oeid in filters else True - -@registerDecorator -class OETestID(OETestFilter): - attrs = ('oeid',) - - def bind(self, registry, case): - super(OETestID, self).bind(registry, case) - - def filtrate(self, filters): - if filters.get('oeid'): - filterx = intToList(filters['oeid'], 'oeid') - del filters['oeid'] - if _idFilter(self.oeid, filterx): - return True - return False diff --git a/meta/lib/oeqa/core/decorator/oetag.py b/meta/lib/oeqa/core/decorator/oetag.py deleted file mode 100644 index ad38ab78a5..0000000000 --- a/meta/lib/oeqa/core/decorator/oetag.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -from . import OETestFilter, registerDecorator -from oeqa.core.utils.misc import strToList - -def _tagFilter(tags, filters): - return False if set(tags) & set(filters) else True - -@registerDecorator -class OETestTag(OETestFilter): - attrs = ('oetag',) - - def bind(self, registry, case): - super(OETestTag, self).bind(registry, case) - self.oetag = strToList(self.oetag, 'oetag') - - def filtrate(self, filters): - if filters.get('oetag'): - filterx = strToList(filters['oetag'], 'oetag') - del filters['oetag'] - if _tagFilter(self.oetag, filterx): - return True - return False diff --git a/meta/lib/oeqa/core/decorator/oetimeout.py b/meta/lib/oeqa/core/decorator/oetimeout.py index a247583f7f..5e6873ad48 100644 --- a/meta/lib/oeqa/core/decorator/oetimeout.py +++ b/meta/lib/oeqa/core/decorator/oetimeout.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import signal from . import OETestDecorator, registerDecorator @@ -21,5 +24,6 @@ class OETimeout(OETestDecorator): def tearDownDecorator(self): signal.alarm(0) - signal.signal(signal.SIGALRM, self.alarmSignal) - self.logger.debug("Removed SIGALRM handler") + if hasattr(self, 'alarmSignal'): + signal.signal(signal.SIGALRM, self.alarmSignal) + self.logger.debug("Removed SIGALRM handler") diff --git a/meta/lib/oeqa/core/exception.py b/meta/lib/oeqa/core/exception.py index 732f2efdeb..05be0ed21f 100644 --- a/meta/lib/oeqa/core/exception.py +++ b/meta/lib/oeqa/core/exception.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# class OEQAException(Exception): pass diff --git a/meta/lib/oeqa/core/loader.py b/meta/lib/oeqa/core/loader.py index e66de32cb1..11978213b8 100644 --- a/meta/lib/oeqa/core/loader.py +++ b/meta/lib/oeqa/core/loader.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import re @@ -13,7 +16,7 @@ from oeqa.core.utils.test import getSuiteModules, getCaseID from oeqa.core.exception import OEQATestNotFound from oeqa.core.case import OETestCase from oeqa.core.decorator import decoratorClasses, OETestDecorator, \ - OETestFilter, OETestDiscover + OETestDiscover # When loading tests, the unittest framework stores any exceptions and # displays them only when the run method is called. @@ -43,7 +46,7 @@ def _built_modules_dict(modules): for module in modules: # Assumption: package and module names do not contain upper case # characters, whereas class names do - m = re.match(r'^(\w+)(?:\.(\w[^.]*)(?:\.([^.]+))?)?$', module, flags=re.ASCII) + m = re.match(r'^([0-9a-z_.]+)(?:\.(\w[^.]*)(?:\.([^.]+))?)?$', module, flags=re.ASCII) if not m: continue @@ -65,7 +68,7 @@ class OETestLoader(unittest.TestLoader): '_top_level_dir'] def __init__(self, tc, module_paths, modules, tests, modules_required, - filters, *args, **kwargs): + *args, **kwargs): self.tc = tc self.modules = _built_modules_dict(modules) @@ -73,13 +76,7 @@ class OETestLoader(unittest.TestLoader): self.tests = tests self.modules_required = modules_required - self.filters = filters - self.decorator_filters = [d for d in decoratorClasses if \ - issubclass(d, OETestFilter)] - self._validateFilters(self.filters, self.decorator_filters) - self.used_filters = [d for d in self.decorator_filters - for f in self.filters - if f in d.attrs] + self.tags_filter = kwargs.get("tags_filter", None) if isinstance(module_paths, str): module_paths = [module_paths] @@ -101,28 +98,6 @@ class OETestLoader(unittest.TestLoader): setattr(testCaseClass, 'td', self.tc.td) setattr(testCaseClass, 'logger', self.tc.logger) - def _validateFilters(self, filters, decorator_filters): - # Validate if filter isn't empty - for key,value in filters.items(): - if not value: - raise TypeError("Filter %s specified is empty" % key) - - # Validate unique attributes - attr_filters = [attr for clss in decorator_filters \ - for attr in clss.attrs] - dup_attr = [attr for attr in attr_filters - if attr_filters.count(attr) > 1] - if dup_attr: - raise TypeError('Detected duplicated attribute(s) %s in filter' - ' decorators' % ' ,'.join(dup_attr)) - - # Validate if filter is supported - for f in filters: - if f not in attr_filters: - classes = ', '.join([d.__name__ for d in decorator_filters]) - raise TypeError('Found "%s" filter but not declared in any of ' - '%s decorators' % (f, classes)) - def _registerTestCase(self, case): case_id = case.id() self.tc._registry['cases'][case_id] = case @@ -185,19 +160,20 @@ class OETestLoader(unittest.TestLoader): return True # Decorator filters - if self.filters and isinstance(case, OETestCase): - filters = self.filters.copy() - case_decorators = [cd for cd in case.decorators - if cd.__class__ in self.used_filters] - - # Iterate over case decorators to check if needs to be filtered. - for cd in case_decorators: - if cd.filtrate(filters): - return True - - # Case is missing one or more decorators for all the filters - # being used, so filter test case. - if filters: + if self.tags_filter is not None and callable(self.tags_filter): + alltags = set() + # pull tags from the case class + if hasattr(case, "__oeqa_testtags"): + for t in getattr(case, "__oeqa_testtags"): + alltags.add(t) + # pull tags from the method itself + if hasattr(case, test_name): + method = getattr(case, test_name) + if hasattr(method, "__oeqa_testtags"): + for t in getattr(method, "__oeqa_testtags"): + alltags.add(t) + + if self.tags_filter(alltags): return True return False diff --git a/meta/lib/oeqa/core/runner.py b/meta/lib/oeqa/core/runner.py index df88b85f1c..d50690ab37 100644 --- a/meta/lib/oeqa/core/runner.py +++ b/meta/lib/oeqa/core/runner.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import time @@ -7,6 +10,7 @@ import unittest import logging import re import json +import sys from unittest import TextTestResult as _TestResult from unittest import TextTestRunner as _TestRunner @@ -39,12 +43,16 @@ class OETestResult(_TestResult): self.starttime = {} self.endtime = {} self.progressinfo = {} + self.extraresults = {} # Inject into tc so that TestDepends decorator can see results tc.results = self self.tc = tc + # stdout and stderr for each test case + self.logged_output = {} + def startTest(self, test): # May have been set by concurrencytest if test.id() not in self.starttime: @@ -53,6 +61,9 @@ class OETestResult(_TestResult): def stopTest(self, test): self.endtime[test.id()] = time.time() + if self.buffer: + self.logged_output[test.id()] = ( + sys.stdout.getvalue(), sys.stderr.getvalue()) super(OETestResult, self).stopTest(test) if test.id() in self.progressinfo: self.tc.logger.info(self.progressinfo[test.id()]) @@ -81,11 +92,17 @@ class OETestResult(_TestResult): def _getTestResultDetails(self, case): result_types = {'failures': 'FAILED', 'errors': 'ERROR', 'skipped': 'SKIPPED', - 'expectedFailures': 'EXPECTEDFAIL', 'successes': 'PASSED'} + 'expectedFailures': 'EXPECTEDFAIL', 'successes': 'PASSED', + 'unexpectedSuccesses' : 'PASSED'} for rtype in result_types: found = False - for (scase, msg) in getattr(self, rtype): + for resultclass in getattr(self, rtype): + # unexpectedSuccesses are just lists, not lists of tuples + if isinstance(resultclass, tuple): + scase, msg = resultclass + else: + scase, msg = resultclass, None if case.id() == scase.id(): found = True break @@ -93,13 +110,13 @@ class OETestResult(_TestResult): # When fails at module or class level the class name is passed as string # so figure out to see if match - m = re.search(r"^setUpModule \((?P<module_name>.*)\)$", scase_str) + m = re.search(r"^setUpModule \((?P<module_name>.*)\).*$", scase_str) if m: if case.__class__.__module__ == m.group('module_name'): found = True break - m = re.search(r"^setUpClass \((?P<class_name>.*)\)$", scase_str) + m = re.search(r"^setUpClass \((?P<class_name>.*)\).*$", scase_str) if m: class_name = "%s.%s" % (case.__class__.__module__, case.__class__.__name__) @@ -113,41 +130,90 @@ class OETestResult(_TestResult): return 'UNKNOWN', None - def addSuccess(self, test): + def extractExtraResults(self, test, details = None): + extraresults = None + if details is not None and "extraresults" in details: + extraresults = details.get("extraresults", {}) + elif hasattr(test, "extraresults"): + extraresults = test.extraresults + + if extraresults is not None: + for k, v in extraresults.items(): + # handle updating already existing entries (e.g. ptestresults.sections) + if k in self.extraresults: + self.extraresults[k].update(v) + else: + self.extraresults[k] = v + + def addError(self, test, *args, details = None): + self.extractExtraResults(test, details = details) + return super(OETestResult, self).addError(test, *args) + + def addFailure(self, test, *args, details = None): + self.extractExtraResults(test, details = details) + return super(OETestResult, self).addFailure(test, *args) + + def addSuccess(self, test, details = None): #Added so we can keep track of successes too self.successes.append((test, None)) - super(OETestResult, self).addSuccess(test) + self.extractExtraResults(test, details = details) + return super(OETestResult, self).addSuccess(test) + + def addExpectedFailure(self, test, *args, details = None): + self.extractExtraResults(test, details = details) + return super(OETestResult, self).addExpectedFailure(test, *args) + + def addUnexpectedSuccess(self, test, details = None): + self.extractExtraResults(test, details = details) + return super(OETestResult, self).addUnexpectedSuccess(test) - def logDetails(self, json_file_dir=None, configuration=None, result_id=None): + def logDetails(self, json_file_dir=None, configuration=None, result_id=None, + dump_streams=False): self.tc.logger.info("RESULTS:") - result = {} + result = self.extraresults logs = {} if hasattr(self.tc, "extraresults"): - result = self.tc.extraresults + result.update(self.tc.extraresults) for case_name in self.tc._registry['cases']: case = self.tc._registry['cases'][case_name] (status, log) = self._getTestResultDetails(case) - oeid = -1 - if hasattr(case, 'decorators'): - for d in case.decorators: - if hasattr(d, 'oeid'): - oeid = d.oeid - t = "" + duration = 0 if case.id() in self.starttime and case.id() in self.endtime: - t = " (" + "{0:.2f}".format(self.endtime[case.id()] - self.starttime[case.id()]) + "s)" + duration = self.endtime[case.id()] - self.starttime[case.id()] + t = " (" + "{0:.2f}".format(duration) + "s)" if status not in logs: logs[status] = [] - logs[status].append("RESULTS - %s - Testcase %s: %s%s" % (case.id(), oeid, status, t)) + logs[status].append("RESULTS - %s: %s%s" % (case.id(), status, t)) + report = {'status': status} if log: - result[case.id()] = {'status': status, 'log': log} - else: - result[case.id()] = {'status': status} + report['log'] = log + if duration: + report['duration'] = duration + + alltags = [] + # pull tags from the case class + if hasattr(case, "__oeqa_testtags"): + alltags.extend(getattr(case, "__oeqa_testtags")) + # pull tags from the method itself + test_name = case._testMethodName + if hasattr(case, test_name): + method = getattr(case, test_name) + if hasattr(method, "__oeqa_testtags"): + alltags.extend(getattr(method, "__oeqa_testtags")) + if alltags: + report['oetags'] = alltags + + if dump_streams and case.id() in self.logged_output: + (stdout, stderr) = self.logged_output[case.id()] + report['stdout'] = stdout + report['stderr'] = stderr + result[case.id()] = report for i in ['PASSED', 'SKIPPED', 'EXPECTEDFAIL', 'ERROR', 'FAILED', 'UNKNOWN']: if i not in logs: @@ -190,42 +256,20 @@ class OETestRunner(_TestRunner): self._walked_cases = self._walked_cases + 1 def _list_tests_name(self, suite): - from oeqa.core.decorator.oeid import OETestID - from oeqa.core.decorator.oetag import OETestTag - self._walked_cases = 0 - def _list_cases_without_id(logger, case): - - found_id = False - if hasattr(case, 'decorators'): - for d in case.decorators: - if isinstance(d, OETestID): - found_id = True - - if not found_id: - logger.info('oeid missing for %s' % case.id()) - def _list_cases(logger, case): - oeid = None - oetag = None - - if hasattr(case, 'decorators'): - for d in case.decorators: - if isinstance(d, OETestID): - oeid = d.oeid - elif isinstance(d, OETestTag): - oetag = d.oetag - - logger.info("%s\t%s\t\t%s" % (oeid, oetag, case.id())) - - self.tc.logger.info("Listing test cases that don't have oeid ...") - self._walk_suite(suite, _list_cases_without_id) - self.tc.logger.info("-" * 80) + oetags = [] + if hasattr(case, '__oeqa_testtags'): + oetags = getattr(case, '__oeqa_testtags') + if oetags: + logger.info("%s (%s)" % (case.id(), ",".join(oetags))) + else: + logger.info("%s" % (case.id())) self.tc.logger.info("Listing all available tests:") self._walked_cases = 0 - self.tc.logger.info("id\ttag\t\ttest") + self.tc.logger.info("test (tags)") self.tc.logger.info("-" * 80) self._walk_suite(suite, _list_cases) self.tc.logger.info("-" * 80) @@ -293,10 +337,17 @@ class OETestResultJSONHelper(object): the_file.write(file_content) def dump_testresult_file(self, write_dir, configuration, result_id, test_result): - bb.utils.mkdirhier(write_dir) - lf = bb.utils.lockfile(os.path.join(write_dir, 'jsontestresult.lock')) + try: + import bb + has_bb = True + bb.utils.mkdirhier(write_dir) + lf = bb.utils.lockfile(os.path.join(write_dir, 'jsontestresult.lock')) + except ImportError: + has_bb = False + os.makedirs(write_dir, exist_ok=True) test_results = self._get_existing_testresults_if_available(write_dir) test_results[result_id] = {'configuration': configuration, 'result': test_result} json_testresults = json.dumps(test_results, sort_keys=True, indent=4) self._write_file(write_dir, self.testresult_filename, json_testresults) - bb.utils.unlockfile(lf) + if has_bb: + bb.utils.unlockfile(lf) diff --git a/meta/lib/oeqa/core/target/__init__.py b/meta/lib/oeqa/core/target/__init__.py index d2468bc257..1382aa9b52 100644 --- a/meta/lib/oeqa/core/target/__init__.py +++ b/meta/lib/oeqa/core/target/__init__.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from abc import abstractmethod diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py index 7a161a3231..79fd724f7d 100644 --- a/meta/lib/oeqa/core/target/qemu.py +++ b/meta/lib/oeqa/core/target/qemu.py @@ -1,13 +1,21 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import sys import signal import time +import glob +import subprocess +from collections import defaultdict from .ssh import OESSHTarget from oeqa.utils.qemurunner import QemuRunner +from oeqa.utils.dump import MonitorDumper +from oeqa.utils.dump import TargetDumper supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] @@ -15,23 +23,39 @@ class OEQemuTarget(OESSHTarget): def __init__(self, logger, server_ip, timeout=300, user='root', port=None, machine='', rootfs='', kernel='', kvm=False, slirp=False, dump_dir='', dump_host_cmds='', display='', bootlog='', - tmpdir='', dir_image='', boottime=60, **kwargs): + tmpdir='', dir_image='', boottime=60, serial_ports=2, + boot_patterns = defaultdict(str), ovmf=False, tmpfsdir=None, **kwargs): super(OEQemuTarget, self).__init__(logger, None, server_ip, timeout, user, port) self.server_ip = server_ip + self.server_port = 0 self.machine = machine self.rootfs = rootfs self.kernel = kernel self.kvm = kvm + self.ovmf = ovmf self.use_slirp = slirp + self.boot_patterns = boot_patterns + self.dump_dir = dump_dir + self.bootlog = bootlog self.runner = QemuRunner(machine=machine, rootfs=rootfs, tmpdir=tmpdir, deploy_dir_image=dir_image, display=display, logfile=bootlog, boottime=boottime, use_kvm=kvm, use_slirp=slirp, dump_dir=dump_dir, - dump_host_cmds=dump_host_cmds, logger=logger) + dump_host_cmds=dump_host_cmds, logger=logger, + serial_ports=serial_ports, boot_patterns = boot_patterns, + use_ovmf=ovmf, tmpfsdir=tmpfsdir) + dump_monitor_cmds = kwargs.get("testimage_dump_monitor") + self.monitor_dumper = MonitorDumper(dump_monitor_cmds, dump_dir, self.runner) + if self.monitor_dumper: + self.monitor_dumper.create_dir("qmp") + + dump_target_cmds = kwargs.get("testimage_dump_target") + self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) + self.target_dumper.create_dir("qemu") def start(self, params=None, extra_bootparams=None, runqemuparams=''): if self.use_slirp and not self.server_ip: @@ -54,7 +78,28 @@ class OEQemuTarget(OESSHTarget): self.server_ip = self.runner.server_ip else: self.stop() - raise RuntimeError("FAILED to start qemu - check the task log and the boot log") + # Display the first 20 lines of top and + # last 20 lines of the bootlog when the + # target is not being booted up. + topfile = glob.glob(self.dump_dir + "/*_qemu/host_*_top") + msg = "\n\n===== start: snippet =====\n\n" + for f in topfile: + msg += "file: %s\n\n" % f + with open(f) as tf: + for x in range(20): + msg += next(tf) + msg += "\n\n===== end: snippet =====\n\n" + blcmd = ["tail", "-20", self.bootlog] + msg += "===== start: snippet =====\n\n" + try: + out = subprocess.check_output(blcmd, stderr=subprocess.STDOUT, timeout=1).decode('utf-8') + msg += "file: %s\n\n" % self.bootlog + msg += out + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as err: + msg += "Error running command: %s\n%s\n" % (blcmd, err) + msg += "\n\n===== end: snippet =====\n" + + raise RuntimeError("FAILED to start qemu - check the task log and the boot log %s" % (msg)) def stop(self): self.runner.stop() diff --git a/meta/lib/oeqa/core/target/ssh.py b/meta/lib/oeqa/core/target/ssh.py index 8ff1f6c677..f956a7744f 100644 --- a/meta/lib/oeqa/core/target/ssh.py +++ b/meta/lib/oeqa/core/target/ssh.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import time @@ -12,7 +15,7 @@ from . import OETarget class OESSHTarget(OETarget): def __init__(self, logger, ip, server_ip, timeout=300, user='root', - port=None, **kwargs): + port=None, server_port=0, **kwargs): if not logger: logger = logging.getLogger('target') logger.setLevel(logging.INFO) @@ -27,6 +30,7 @@ class OESSHTarget(OETarget): super(OESSHTarget, self).__init__(logger) self.ip = ip self.server_ip = server_ip + self.server_port = server_port self.timeout = timeout self.user = user ssh_options = [ @@ -39,6 +43,8 @@ class OESSHTarget(OETarget): if port: self.ssh = self.ssh + [ '-p', port ] self.scp = self.scp + [ '-P', port ] + self._monitor_dumper = None + self.target_dumper = None def start(self, **kwargs): pass @@ -46,6 +52,15 @@ class OESSHTarget(OETarget): def stop(self, **kwargs): pass + @property + def monitor_dumper(self): + return self._monitor_dumper + + @monitor_dumper.setter + def monitor_dumper(self, dumper): + self._monitor_dumper = dumper + self.monitor_dumper.dump_monitor() + def _run(self, command, timeout=None, ignore_status=True): """ Runs command in target using SSHProcess. @@ -83,7 +98,15 @@ class OESSHTarget(OETarget): processTimeout = self.timeout status, output = self._run(sshCmd, processTimeout, True) - self.logger.debug('Command: %s\nOutput: %s\n' % (command, output)) + self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) + if (status == 255) and (('No route to host') in output): + if self.monitor_dumper: + self.monitor_dumper.dump_monitor() + if status == 255: + if self.target_dumper: + self.target_dumper.dump_target() + if self.monitor_dumper: + self.monitor_dumper.dump_monitor() return (status, output) def copyTo(self, localSrc, remoteDst): @@ -103,13 +126,16 @@ class OESSHTarget(OETarget): scpCmd = self.scp + [localSrc, remotePath] return self._run(scpCmd, ignore_status=False) - def copyFrom(self, remoteSrc, localDst): + def copyFrom(self, remoteSrc, localDst, warn_on_failure=False): """ Copy file from target. """ remotePath = '%s@%s:%s' % (self.user, self.ip, remoteSrc) scpCmd = self.scp + [remotePath, localDst] - return self._run(scpCmd, ignore_status=False) + (status, output) = self._run(scpCmd, ignore_status=warn_on_failure) + if warn_on_failure and status: + self.logger.warning("Copy returned non-zero exit status %d:\n%s" % (status, output)) + return (status, output) def copyDirTo(self, localSrc, remoteDst): """ @@ -207,7 +233,7 @@ def SSHCall(command, logger, timeout=None, **opts): logger.debug('time: %s, endtime: %s' % (time.time(), endtime)) try: if select.select([process.stdout], [], [], 5)[0] != []: - reader = codecs.getreader('utf-8')(process.stdout) + reader = codecs.getreader('utf-8')(process.stdout, 'ignore') data = reader.read(1024, 4096) if not data: process.stdout.close() @@ -234,7 +260,7 @@ def SSHCall(command, logger, timeout=None, **opts): output += lastline else: - output = process.communicate()[0].decode("utf-8", errors='replace') + output = process.communicate()[0].decode('utf-8', errors='ignore') logger.debug('Data from SSH call: %s' % output.rstrip()) options = { @@ -243,7 +269,7 @@ def SSHCall(command, logger, timeout=None, **opts): "stdin": None, "shell": False, "bufsize": -1, - "preexec_fn": os.setsid, + "start_new_session": True, } options.update(opts) output = '' diff --git a/meta/lib/oeqa/core/tests/cases/data.py b/meta/lib/oeqa/core/tests/cases/data.py index 88003a6adc..61f88547f7 100644 --- a/meta/lib/oeqa/core/tests/cases/data.py +++ b/meta/lib/oeqa/core/tests/cases/data.py @@ -1,8 +1,11 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.case import OETestCase -from oeqa.core.decorator.oetag import OETestTag +from oeqa.core.decorator import OETestTag from oeqa.core.decorator.data import OETestDataDepends class DataTest(OETestCase): diff --git a/meta/lib/oeqa/core/tests/cases/depends.py b/meta/lib/oeqa/core/tests/cases/depends.py index 17cdd90b15..46e7db900d 100644 --- a/meta/lib/oeqa/core/tests/cases/depends.py +++ b/meta/lib/oeqa/core/tests/cases/depends.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.case import OETestCase from oeqa.core.decorator.depends import OETestDepends diff --git a/meta/lib/oeqa/core/tests/cases/loader/invalid/oeid.py b/meta/lib/oeqa/core/tests/cases/loader/invalid/oeid.py deleted file mode 100644 index 038d445931..0000000000 --- a/meta/lib/oeqa/core/tests/cases/loader/invalid/oeid.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -from oeqa.core.case import OETestCase - -class AnotherIDTest(OETestCase): - - def testAnotherIdGood(self): - self.assertTrue(True, msg='How is this possible?') - - def testAnotherIdOther(self): - self.assertTrue(True, msg='How is this possible?') - - def testAnotherIdNone(self): - self.assertTrue(True, msg='How is this possible?') diff --git a/meta/lib/oeqa/core/tests/cases/loader/valid/another.py b/meta/lib/oeqa/core/tests/cases/loader/valid/another.py index c9ffd17773..bedc20c8a6 100644 --- a/meta/lib/oeqa/core/tests/cases/loader/valid/another.py +++ b/meta/lib/oeqa/core/tests/cases/loader/valid/another.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.case import OETestCase diff --git a/meta/lib/oeqa/core/tests/cases/oeid.py b/meta/lib/oeqa/core/tests/cases/oeid.py deleted file mode 100644 index c2d3d32f2d..0000000000 --- a/meta/lib/oeqa/core/tests/cases/oeid.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -from oeqa.core.case import OETestCase -from oeqa.core.decorator.oeid import OETestID - -class IDTest(OETestCase): - - @OETestID(101) - def testIdGood(self): - self.assertTrue(True, msg='How is this possible?') - - @OETestID(102) - def testIdOther(self): - self.assertTrue(True, msg='How is this possible?') - - def testIdNone(self): - self.assertTrue(True, msg='How is this possible?') diff --git a/meta/lib/oeqa/core/tests/cases/oetag.py b/meta/lib/oeqa/core/tests/cases/oetag.py index 0cae02e75c..52f97dfda6 100644 --- a/meta/lib/oeqa/core/tests/cases/oetag.py +++ b/meta/lib/oeqa/core/tests/cases/oetag.py @@ -1,11 +1,13 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from oeqa.core.case import OETestCase -from oeqa.core.decorator.oetag import OETestTag +from oeqa.core.decorator import OETestTag class TagTest(OETestCase): - @OETestTag('goodTag') def testTagGood(self): self.assertTrue(True, msg='How is this possible?') @@ -14,5 +16,23 @@ class TagTest(OETestCase): def testTagOther(self): self.assertTrue(True, msg='How is this possible?') + @OETestTag('otherTag', 'multiTag') + def testTagOtherMulti(self): + self.assertTrue(True, msg='How is this possible?') + def testTagNone(self): self.assertTrue(True, msg='How is this possible?') + +@OETestTag('classTag') +class TagClassTest(OETestCase): + @OETestTag('otherTag') + def testTagOther(self): + self.assertTrue(True, msg='How is this possible?') + + @OETestTag('otherTag', 'multiTag') + def testTagOtherMulti(self): + self.assertTrue(True, msg='How is this possible?') + + def testTagNone(self): + self.assertTrue(True, msg='How is this possible?') + diff --git a/meta/lib/oeqa/core/tests/cases/timeout.py b/meta/lib/oeqa/core/tests/cases/timeout.py index 870c3157f7..69cf969a67 100644 --- a/meta/lib/oeqa/core/tests/cases/timeout.py +++ b/meta/lib/oeqa/core/tests/cases/timeout.py @@ -1,10 +1,14 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# from time import sleep from oeqa.core.case import OETestCase from oeqa.core.decorator.oetimeout import OETimeout +from oeqa.core.decorator.depends import OETestDepends class TimeoutTest(OETestCase): @@ -16,3 +20,15 @@ class TimeoutTest(OETestCase): def testTimeoutFail(self): sleep(2) self.assertTrue(True, msg='How is this possible?') + + + def testTimeoutSkip(self): + self.skipTest("This test needs to be skipped, so that testTimeoutDepends()'s OETestDepends kicks in") + + @OETestDepends(["timeout.TimeoutTest.testTimeoutSkip"]) + @OETimeout(3) + def testTimeoutDepends(self): + self.assertTrue(False, msg='How is this possible?') + + def testTimeoutUnrelated(self): + sleep(6) diff --git a/meta/lib/oeqa/core/tests/common.py b/meta/lib/oeqa/core/tests/common.py index 52b18a1c3e..88cc758ad3 100644 --- a/meta/lib/oeqa/core/tests/common.py +++ b/meta/lib/oeqa/core/tests/common.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import sys import os @@ -27,9 +30,9 @@ class TestBase(unittest.TestCase): directory = os.path.dirname(os.path.abspath(__file__)) self.cases_path = os.path.join(directory, 'cases') - def _testLoader(self, d={}, modules=[], tests=[], filters={}): + def _testLoader(self, d={}, modules=[], tests=[], **kwargs): from oeqa.core.context import OETestContext tc = OETestContext(d, self.logger) tc.loadTests(self.cases_path, modules=modules, tests=tests, - filters=filters) + **kwargs) return tc diff --git a/meta/lib/oeqa/core/tests/test_data.py b/meta/lib/oeqa/core/tests/test_data.py index 21b6c68b8a..acd726f3a0 100755 --- a/meta/lib/oeqa/core/tests/test_data.py +++ b/meta/lib/oeqa/core/tests/test_data.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import unittest import logging @@ -20,8 +22,9 @@ class TestData(TestBase): expectedException = "oeqa.core.exception.OEQAMissingVariable" tc = self._testLoader(modules=self.modules) - self.assertEqual(False, tc.runTests().wasSuccessful()) - for test, data in tc.errors: + results = tc.runTests() + self.assertFalse(results.wasSuccessful()) + for test, data in results.errors: expect = False if expectedException in data: expect = True @@ -30,11 +33,12 @@ class TestData(TestBase): def test_data_fail_wrong_variable(self): expectedError = 'AssertionError' - d = {'IMAGE' : 'core-image-sato', 'ARCH' : 'arm'} + d = {'IMAGE' : 'core-image-weston', 'ARCH' : 'arm'} tc = self._testLoader(d=d, modules=self.modules) - self.assertEqual(False, tc.runTests().wasSuccessful()) - for test, data in tc.failures: + results = tc.runTests() + self.assertFalse(results.wasSuccessful()) + for test, data in results.failures: expect = False if expectedError in data: expect = True diff --git a/meta/lib/oeqa/core/tests/test_decorators.py b/meta/lib/oeqa/core/tests/test_decorators.py index f7d11e885a..5095f39948 100755 --- a/meta/lib/oeqa/core/tests/test_decorators.py +++ b/meta/lib/oeqa/core/tests/test_decorators.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 - +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import signal import unittest @@ -12,58 +14,58 @@ setup_sys_path() from oeqa.core.exception import OEQADependency from oeqa.core.utils.test import getCaseMethod, getSuiteCasesNames, getSuiteCasesIDs -class TestFilterDecorator(TestBase): - - def _runFilterTest(self, modules, filters, expect, msg): - tc = self._testLoader(modules=modules, filters=filters) - test_loaded = set(getSuiteCasesNames(tc.suites)) - self.assertEqual(expect, test_loaded, msg=msg) +class TestTagDecorator(TestBase): + def _runTest(self, modules, filterfn, expect): + tc = self._testLoader(modules = modules, tags_filter = filterfn) + test_loaded = set(getSuiteCasesIDs(tc.suites)) + self.assertEqual(expect, test_loaded) def test_oetag(self): - # Get all cases without filtering. - filter_all = {} - test_all = {'testTagGood', 'testTagOther', 'testTagNone'} - msg_all = 'Failed to get all oetag cases without filtering.' - - # Get cases with 'goodTag'. - filter_good = {'oetag':'goodTag'} - test_good = {'testTagGood'} - msg_good = 'Failed to get just one test filtering with "goodTag" oetag.' - - # Get cases with an invalid tag. - filter_invalid = {'oetag':'invalidTag'} - test_invalid = set() - msg_invalid = 'Failed to filter all test using an invalid oetag.' - - tests = ((filter_all, test_all, msg_all), - (filter_good, test_good, msg_good), - (filter_invalid, test_invalid, msg_invalid)) - - for test in tests: - self._runFilterTest(['oetag'], test[0], test[1], test[2]) - - def test_oeid(self): - # Get all cases without filtering. - filter_all = {} - test_all = {'testIdGood', 'testIdOther', 'testIdNone'} - msg_all = 'Failed to get all oeid cases without filtering.' - - # Get cases with '101' oeid. - filter_good = {'oeid': 101} - test_good = {'testIdGood'} - msg_good = 'Failed to get just one tes filtering with "101" oeid.' - - # Get cases with an invalid id. - filter_invalid = {'oeid':999} - test_invalid = set() - msg_invalid = 'Failed to filter all test using an invalid oeid.' - - tests = ((filter_all, test_all, msg_all), - (filter_good, test_good, msg_good), - (filter_invalid, test_invalid, msg_invalid)) - - for test in tests: - self._runFilterTest(['oeid'], test[0], test[1], test[2]) + # get all cases without any filtering + self._runTest(['oetag'], None, { + 'oetag.TagTest.testTagGood', + 'oetag.TagTest.testTagOther', + 'oetag.TagTest.testTagOtherMulti', + 'oetag.TagTest.testTagNone', + 'oetag.TagClassTest.testTagOther', + 'oetag.TagClassTest.testTagOtherMulti', + 'oetag.TagClassTest.testTagNone', + }) + + # exclude any case with tags + self._runTest(['oetag'], lambda tags: tags, { + 'oetag.TagTest.testTagNone', + }) + + # exclude any case with otherTag + self._runTest(['oetag'], lambda tags: "otherTag" in tags, { + 'oetag.TagTest.testTagGood', + 'oetag.TagTest.testTagNone', + 'oetag.TagClassTest.testTagNone', + }) + + # exclude any case with classTag + self._runTest(['oetag'], lambda tags: "classTag" in tags, { + 'oetag.TagTest.testTagGood', + 'oetag.TagTest.testTagOther', + 'oetag.TagTest.testTagOtherMulti', + 'oetag.TagTest.testTagNone', + }) + + # include any case with classTag + self._runTest(['oetag'], lambda tags: "classTag" not in tags, { + 'oetag.TagClassTest.testTagOther', + 'oetag.TagClassTest.testTagOtherMulti', + 'oetag.TagClassTest.testTagNone', + }) + + # include any case with classTag or no tags + self._runTest(['oetag'], lambda tags: tags and "classTag" not in tags, { + 'oetag.TagTest.testTagNone', + 'oetag.TagClassTest.testTagOther', + 'oetag.TagClassTest.testTagOtherMulti', + 'oetag.TagClassTest.testTagNone', + }) class TestDependsDecorator(TestBase): modules = ['depends'] @@ -131,5 +133,11 @@ class TestTimeoutDecorator(TestBase): msg = "OETestTimeout didn't restore SIGALRM" self.assertIs(alarm_signal, signal.getsignal(signal.SIGALRM), msg=msg) + def test_timeout_cancel(self): + tests = ['timeout.TimeoutTest.testTimeoutSkip', 'timeout.TimeoutTest.testTimeoutDepends', 'timeout.TimeoutTest.testTimeoutUnrelated'] + msg = 'Unrelated test failed to complete' + tc = self._testLoader(modules=self.modules, tests=tests) + self.assertTrue(tc.runTests().wasSuccessful(), msg=msg) + if __name__ == '__main__': unittest.main() diff --git a/meta/lib/oeqa/core/tests/test_loader.py b/meta/lib/oeqa/core/tests/test_loader.py index b79b8bad4d..cb38ac845e 100755 --- a/meta/lib/oeqa/core/tests/test_loader.py +++ b/meta/lib/oeqa/core/tests/test_loader.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 - +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import unittest @@ -13,36 +15,12 @@ from oeqa.core.exception import OEQADependency from oeqa.core.utils.test import getSuiteModules, getSuiteCasesIDs class TestLoader(TestBase): - - def test_fail_empty_filter(self): - filters = {'oetag' : ''} - expect = 'Filter oetag specified is empty' - msg = 'Expected TypeError exception for having invalid filter' - try: - # Must throw TypeError because empty filter - tc = self._testLoader(filters=filters) - self.fail(msg) - except TypeError as e: - result = True if expect in str(e) else False - self.assertTrue(result, msg=msg) - - def test_fail_invalid_filter(self): - filters = {'invalid' : 'good'} - expect = 'filter but not declared in any of' - msg = 'Expected TypeError exception for having invalid filter' - try: - # Must throw TypeError because invalid filter - tc = self._testLoader(filters=filters) - self.fail(msg) - except TypeError as e: - result = True if expect in str(e) else False - self.assertTrue(result, msg=msg) - + @unittest.skip("invalid directory is missing oetag.py") def test_fail_duplicated_module(self): cases_path = self.cases_path invalid_path = os.path.join(cases_path, 'loader', 'invalid') self.cases_path = [self.cases_path, invalid_path] - expect = 'Duplicated oeid module found in' + expect = 'Duplicated oetag module found in' msg = 'Expected ImportError exception for having duplicated module' try: # Must throw ImportEror because duplicated module @@ -55,17 +33,16 @@ class TestLoader(TestBase): self.cases_path = cases_path def test_filter_modules(self): - expected_modules = {'oeid', 'oetag'} + expected_modules = {'oetag'} tc = self._testLoader(modules=expected_modules) modules = getSuiteModules(tc.suites) msg = 'Expected just %s modules' % ', '.join(expected_modules) self.assertEqual(modules, expected_modules, msg=msg) def test_filter_cases(self): - modules = ['oeid', 'oetag', 'data'] + modules = ['oetag', 'data'] expected_cases = {'data.DataTest.testDataOk', - 'oetag.TagTest.testTagGood', - 'oeid.IDTest.testIdGood'} + 'oetag.TagTest.testTagGood'} tc = self._testLoader(modules=modules, tests=expected_cases) cases = set(getSuiteCasesIDs(tc.suites)) msg = 'Expected just %s cases' % ', '.join(expected_cases) @@ -74,7 +51,7 @@ class TestLoader(TestBase): def test_import_from_paths(self): cases_path = self.cases_path cases2_path = os.path.join(cases_path, 'loader', 'valid') - expected_modules = {'oeid', 'another'} + expected_modules = {'another'} self.cases_path = [self.cases_path, cases2_path] tc = self._testLoader(modules=expected_modules) modules = getSuiteModules(tc.suites) diff --git a/meta/lib/oeqa/core/tests/test_runner.py b/meta/lib/oeqa/core/tests/test_runner.py index a3f3861fed..205464cfae 100755 --- a/meta/lib/oeqa/core/tests/test_runner.py +++ b/meta/lib/oeqa/core/tests/test_runner.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 - +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import unittest import logging diff --git a/meta/lib/oeqa/core/utils/concurrencytest.py b/meta/lib/oeqa/core/utils/concurrencytest.py index e050818f0f..161a2f6e90 100644 --- a/meta/lib/oeqa/core/utils/concurrencytest.py +++ b/meta/lib/oeqa/core/utils/concurrencytest.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # +# SPDX-License-Identifier: GPL-2.0-or-later +# # Modified for use in OE by Richard Purdie, 2018 # # Modified by: Corey Goldberg, 2013 @@ -19,12 +21,16 @@ import testtools import threading import time import io +import json +import subunit from queue import Queue from itertools import cycle from subunit import ProtocolTestCase, TestProtocolClient from subunit.test_results import AutoTimingTestResultDecorator from testtools import ThreadsafeForwardingResult, iterate_tests +from testtools.content import Content +from testtools.content_type import ContentType from oeqa.utils.commands import get_test_layer import bb.utils @@ -42,19 +48,24 @@ _all__ = [ # class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): - def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests): + def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult): super(BBThreadsafeForwardingResult, self).__init__(target, semaphore) self.threadnum = threadnum self.totalinprocess = totalinprocess self.totaltests = totaltests + self.buffer = True + self.outputbuf = output + self.finalresult = finalresult + self.finalresult.buffer = True def _add_result_with_semaphore(self, method, test, *args, **kwargs): self.semaphore.acquire() try: - self.result.starttime[test.id()] = self._test_start.timestamp() - self.result.threadprogress[self.threadnum].append(test.id()) - totalprogress = sum(len(x) for x in self.result.threadprogress.values()) - self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s)" % ( + if self._test_start: + self.result.starttime[test.id()] = self._test_start.timestamp() + self.result.threadprogress[self.threadnum].append(test.id()) + totalprogress = sum(len(x) for x in self.result.threadprogress.values()) + self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s)" % ( self.threadnum, len(self.result.threadprogress[self.threadnum]), self.totalinprocess, @@ -64,8 +75,104 @@ class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): test.id()) finally: self.semaphore.release() + self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8")) + self.finalresult._stdout_buffer = io.StringIO() super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs) +class ProxyTestResult: + # a very basic TestResult proxy, in order to modify add* calls + def __init__(self, target): + self.result = target + self.failed_tests = 0 + + def _addResult(self, method, test, *args, exception = False, **kwargs): + return method(test, *args, **kwargs) + + def addError(self, test, err = None, **kwargs): + self.failed_tests += 1 + self._addResult(self.result.addError, test, err, exception = True, **kwargs) + + def addFailure(self, test, err = None, **kwargs): + self.failed_tests += 1 + self._addResult(self.result.addFailure, test, err, exception = True, **kwargs) + + def addSuccess(self, test, **kwargs): + self._addResult(self.result.addSuccess, test, **kwargs) + + def addExpectedFailure(self, test, err = None, **kwargs): + self._addResult(self.result.addExpectedFailure, test, err, exception = True, **kwargs) + + def addUnexpectedSuccess(self, test, **kwargs): + self._addResult(self.result.addUnexpectedSuccess, test, **kwargs) + + def wasSuccessful(self): + return self.failed_tests == 0 + + def __getattr__(self, attr): + return getattr(self.result, attr) + +class ExtraResultsDecoderTestResult(ProxyTestResult): + def _addResult(self, method, test, *args, exception = False, **kwargs): + if "details" in kwargs and "extraresults" in kwargs["details"]: + if isinstance(kwargs["details"]["extraresults"], Content): + kwargs = kwargs.copy() + kwargs["details"] = kwargs["details"].copy() + extraresults = kwargs["details"]["extraresults"] + data = bytearray() + for b in extraresults.iter_bytes(): + data += b + extraresults = json.loads(data.decode()) + kwargs["details"]["extraresults"] = extraresults + return method(test, *args, **kwargs) + +class ExtraResultsEncoderTestResult(ProxyTestResult): + def _addResult(self, method, test, *args, exception = False, **kwargs): + if hasattr(test, "extraresults"): + extras = lambda : [json.dumps(test.extraresults).encode()] + kwargs = kwargs.copy() + if "details" not in kwargs: + kwargs["details"] = {} + else: + kwargs["details"] = kwargs["details"].copy() + kwargs["details"]["extraresults"] = Content(ContentType("application", "json", {'charset': 'utf8'}), extras) + # if using details, need to encode any exceptions into the details obj, + # testtools does not handle "err" and "details" together. + if "details" in kwargs and exception and (len(args) >= 1 and args[0] is not None): + kwargs["details"]["traceback"] = testtools.content.TracebackContent(args[0], test) + args = [] + return method(test, *args, **kwargs) + +# +# We have to patch subunit since it doesn't understand how to handle addError +# outside of a running test case. This can happen if classSetUp() fails +# for a class of tests. This unfortunately has horrible internal knowledge. +# +def outSideTestaddError(self, offset, line): + """An 'error:' directive has been read.""" + test_name = line[offset:-1].decode('utf8') + self.parser._current_test = subunit.RemotedTestCase(test_name) + self.parser.current_test_description = test_name + self.parser._state = self.parser._reading_error_details + self.parser._reading_error_details.set_simple() + self.parser.subunitLineReceived(line) + +subunit._OutSideTest.addError = outSideTestaddError + +# Like outSideTestaddError above, we need an equivalent for skips +# happening at the setUpClass() level, otherwise we will see "UNKNOWN" +# as a result for concurrent tests +# +def outSideTestaddSkip(self, offset, line): + """A 'skip:' directive has been read.""" + test_name = line[offset:-1].decode('utf8') + self.parser._current_test = subunit.RemotedTestCase(test_name) + self.parser.current_test_description = test_name + self.parser._state = self.parser._reading_skip_details + self.parser._reading_skip_details.set_simple() + self.parser.subunitLineReceived(line) + +subunit._OutSideTest.addSkip = outSideTestaddSkip + # # A dummy structure to add to io.StringIO so that the .buffer object # is available and accepts writes. This allows unittest with buffer=True @@ -82,31 +189,27 @@ class dummybuf(object): # class ConcurrentTestSuite(unittest.TestSuite): - def __init__(self, suite, processes): + def __init__(self, suite, processes, setupfunc, removefunc): super(ConcurrentTestSuite, self).__init__([suite]) self.processes = processes + self.setupfunc = setupfunc + self.removefunc = removefunc def run(self, result): - tests, totaltests = fork_for_tests(self.processes, self) + testservers, totaltests = fork_for_tests(self.processes, self) try: threads = {} queue = Queue() semaphore = threading.Semaphore(1) result.threadprogress = {} - for i, (test, testnum) in enumerate(tests): + for i, (testserver, testnum, output) in enumerate(testservers): result.threadprogress[i] = [] - process_result = BBThreadsafeForwardingResult(result, semaphore, i, testnum, totaltests) - # Force buffering of stdout/stderr so the console doesn't get corrupted by test output - # as per default in parent code - process_result.buffer = True - # We have to add a buffer object to stdout to keep subunit happy - process_result._stderr_buffer = io.StringIO() - process_result._stderr_buffer.buffer = dummybuf(process_result._stderr_buffer) - process_result._stdout_buffer = io.StringIO() - process_result._stdout_buffer.buffer = dummybuf(process_result._stdout_buffer) + process_result = BBThreadsafeForwardingResult( + ExtraResultsDecoderTestResult(result), + semaphore, i, testnum, totaltests, output, result) reader_thread = threading.Thread( - target=self._run_test, args=(test, process_result, queue)) - threads[test] = reader_thread, process_result + target=self._run_test, args=(testserver, process_result, queue)) + threads[testserver] = reader_thread, process_result reader_thread.start() while threads: finished_test = queue.get() @@ -117,13 +220,13 @@ class ConcurrentTestSuite(unittest.TestSuite): process_result.stop() raise finally: - for test in tests: - test[0]._stream.close() + for testserver in testservers: + testserver[0]._stream.close() - def _run_test(self, test, process_result, queue): + def _run_test(self, testserver, process_result, queue): try: try: - test.run(process_result) + testserver.run(process_result) except Exception: # The run logic itself failed case = testtools.ErrorHolder( @@ -131,17 +234,10 @@ class ConcurrentTestSuite(unittest.TestSuite): error=sys.exc_info()) case.run(process_result) finally: - queue.put(test) - -def removebuilddir(d): - delay = 5 - while delay and os.path.exists(d + "/bitbake.lock"): - time.sleep(1) - delay = delay - 1 - bb.utils.prunedir(d) + queue.put(testserver) def fork_for_tests(concurrency_num, suite): - result = [] + testservers = [] if 'BUILDDIR' in os.environ: selftestdir = get_test_layer() @@ -166,37 +262,7 @@ def fork_for_tests(concurrency_num, suite): stream = os.fdopen(c2pwrite, 'wb', 1) os.close(c2pread) - # Create a new separate BUILDDIR for each group of tests - if 'BUILDDIR' in os.environ: - builddir = os.environ['BUILDDIR'] - newbuilddir = builddir + "-st-" + str(ourpid) - newselftestdir = newbuilddir + "/meta-selftest" - - bb.utils.mkdirhier(newbuilddir) - oe.path.copytree(builddir + "/conf", newbuilddir + "/conf") - oe.path.copytree(builddir + "/cache", newbuilddir + "/cache") - oe.path.copytree(selftestdir, newselftestdir) - - for e in os.environ: - if builddir in os.environ[e]: - os.environ[e] = os.environ[e].replace(builddir, newbuilddir) - - subprocess.check_output("git init; git add *; git commit -a -m 'initial'", cwd=newselftestdir, shell=True) - - # Tried to used bitbake-layers add/remove but it requires recipe parsing and hence is too slow - subprocess.check_output("sed %s/conf/bblayers.conf -i -e 's#%s#%s#g'" % (newbuilddir, selftestdir, newselftestdir), cwd=newbuilddir, shell=True) - - os.chdir(newbuilddir) - - for t in process_suite: - if not hasattr(t, "tc"): - continue - cp = t.tc.config_paths - for p in cp: - if selftestdir in cp[p] and newselftestdir not in cp[p]: - cp[p] = cp[p].replace(selftestdir, newselftestdir) - if builddir in cp[p] and newbuilddir not in cp[p]: - cp[p] = cp[p].replace(builddir, newbuilddir) + (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite) # Leave stderr and stdout open so we can see test noise # Close stdin so that the child goes away if it decides to @@ -205,16 +271,17 @@ def fork_for_tests(concurrency_num, suite): newsi = os.open(os.devnull, os.O_RDWR) os.dup2(newsi, sys.stdin.fileno()) + # Send stdout/stderr over the stream + os.dup2(c2pwrite, sys.stdout.fileno()) + os.dup2(c2pwrite, sys.stderr.fileno()) + subunit_client = TestProtocolClient(stream) - # Force buffering of stdout/stderr so the console doesn't get corrupted by test output - # as per default in parent code - subunit_client.buffer = True subunit_result = AutoTimingTestResultDecorator(subunit_client) - process_suite.run(subunit_result) + unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result)) if ourpid != os.getpid(): os._exit(0) - if newbuilddir: - removebuilddir(newbuilddir) + if newbuilddir and unittest_result.wasSuccessful(): + suite.removefunc(newbuilddir) except: # Don't do anything with process children if ourpid != os.getpid(): @@ -230,7 +297,7 @@ def fork_for_tests(concurrency_num, suite): sys.stderr.write(traceback.format_exc()) finally: if newbuilddir: - removebuilddir(newbuilddir) + suite.removefunc(newbuilddir) stream.flush() os._exit(1) stream.flush() @@ -238,9 +305,11 @@ def fork_for_tests(concurrency_num, suite): else: os.close(c2pwrite) stream = os.fdopen(c2pread, 'rb', 1) - test = ProtocolTestCase(stream) - result.append((test, numtests)) - return result, totaltests + # Collect stdout/stderr into an io buffer + output = io.BytesIO() + testserver = ProtocolTestCase(stream, passthrough=output) + testservers.append((testserver, numtests, output)) + return testservers, totaltests def partition_tests(suite, count): # Keep tests from the same class together but allow tests from modules diff --git a/meta/lib/oeqa/core/utils/misc.py b/meta/lib/oeqa/core/utils/misc.py index 0b223b5d08..e1a59588eb 100644 --- a/meta/lib/oeqa/core/utils/misc.py +++ b/meta/lib/oeqa/core/utils/misc.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# def toList(obj, obj_type, obj_name="Object"): if isinstance(obj, obj_type): diff --git a/meta/lib/oeqa/core/utils/path.py b/meta/lib/oeqa/core/utils/path.py index a21caad5cb..c086dcb0b0 100644 --- a/meta/lib/oeqa/core/utils/path.py +++ b/meta/lib/oeqa/core/utils/path.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import sys diff --git a/meta/lib/oeqa/core/utils/test.py b/meta/lib/oeqa/core/utils/test.py index 88d5d13981..d38cab8a51 100644 --- a/meta/lib/oeqa/core/utils/test.py +++ b/meta/lib/oeqa/core/utils/test.py @@ -1,5 +1,8 @@ +# # Copyright (C) 2016 Intel Corporation -# Released under the MIT license (see COPYING.MIT) +# +# SPDX-License-Identifier: MIT +# import os import inspect |