diff options
Diffstat (limited to 'meta/lib/oeqa/core')
| -rw-r--r-- | meta/lib/oeqa/core/case.py | 17 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/context.py | 9 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/decorator/__init__.py | 11 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/decorator/data.py | 86 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/decorator/oetimeout.py | 5 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/loader.py | 12 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/runner.py | 14 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/target/__init__.py | 1 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/target/qemu.py | 42 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/target/serial.py | 315 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/target/ssh.py | 121 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/tests/cases/timeout.py | 13 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/tests/common.py | 1 | ||||
| -rwxr-xr-x | meta/lib/oeqa/core/tests/test_data.py | 2 | ||||
| -rwxr-xr-x | meta/lib/oeqa/core/tests/test_decorators.py | 6 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/utils/concurrencytest.py | 68 | ||||
| -rw-r--r-- | meta/lib/oeqa/core/utils/misc.py | 47 |
17 files changed, 604 insertions, 166 deletions
diff --git a/meta/lib/oeqa/core/case.py b/meta/lib/oeqa/core/case.py index aae451fef2..ad5524a714 100644 --- a/meta/lib/oeqa/core/case.py +++ b/meta/lib/oeqa/core/case.py | |||
| @@ -5,6 +5,7 @@ | |||
| 5 | # | 5 | # |
| 6 | 6 | ||
| 7 | import base64 | 7 | import base64 |
| 8 | import os | ||
| 8 | import zlib | 9 | import zlib |
| 9 | import unittest | 10 | import unittest |
| 10 | 11 | ||
| @@ -43,8 +44,13 @@ class OETestCase(unittest.TestCase): | |||
| 43 | clss.tearDownClassMethod() | 44 | clss.tearDownClassMethod() |
| 44 | 45 | ||
| 45 | def _oeSetUp(self): | 46 | def _oeSetUp(self): |
| 46 | for d in self.decorators: | 47 | try: |
| 47 | d.setUpDecorator() | 48 | for d in self.decorators: |
| 49 | d.setUpDecorator() | ||
| 50 | except: | ||
| 51 | for d in self.decorators: | ||
| 52 | d.tearDownDecorator() | ||
| 53 | raise | ||
| 48 | self.setUpMethod() | 54 | self.setUpMethod() |
| 49 | 55 | ||
| 50 | def _oeTearDown(self): | 56 | def _oeTearDown(self): |
| @@ -52,6 +58,13 @@ class OETestCase(unittest.TestCase): | |||
| 52 | d.tearDownDecorator() | 58 | d.tearDownDecorator() |
| 53 | self.tearDownMethod() | 59 | self.tearDownMethod() |
| 54 | 60 | ||
| 61 | def assertFileExists(self, filename, msg=None): | ||
| 62 | """ | ||
| 63 | Test that filename exists. If it does not, the test will fail. | ||
| 64 | """ | ||
| 65 | if not os.path.exists(filename): | ||
| 66 | self.fail(msg or "%s does not exist" % filename) | ||
| 67 | |||
| 55 | class OEPTestResultTestCase: | 68 | class OEPTestResultTestCase: |
| 56 | """ | 69 | """ |
| 57 | Mix-in class to provide functions to make interacting with extraresults for | 70 | Mix-in class to provide functions to make interacting with extraresults for |
diff --git a/meta/lib/oeqa/core/context.py b/meta/lib/oeqa/core/context.py index 2abe353d27..46de9135c1 100644 --- a/meta/lib/oeqa/core/context.py +++ b/meta/lib/oeqa/core/context.py | |||
| @@ -81,7 +81,7 @@ class OETestContext(object): | |||
| 81 | def runTests(self, processes=None, skips=[]): | 81 | def runTests(self, processes=None, skips=[]): |
| 82 | self.runner = self.runnerClass(self, descriptions=False, verbosity=2) | 82 | self.runner = self.runnerClass(self, descriptions=False, verbosity=2) |
| 83 | 83 | ||
| 84 | # Dinamically skip those tests specified though arguments | 84 | # Dynamically skip those tests specified though arguments |
| 85 | self.skipTests(skips) | 85 | self.skipTests(skips) |
| 86 | 86 | ||
| 87 | self._run_start_time = time.time() | 87 | self._run_start_time = time.time() |
| @@ -179,9 +179,16 @@ class OETestContextExecutor(object): | |||
| 179 | else: | 179 | else: |
| 180 | self.tc_kwargs['init']['td'] = {} | 180 | self.tc_kwargs['init']['td'] = {} |
| 181 | 181 | ||
| 182 | # Run image specific TEST_SUITE like testimage.bbclass by default | ||
| 183 | test_suites = self.tc_kwargs['init']['td'].get("TEST_SUITES") | ||
| 184 | if test_suites: | ||
| 185 | test_suites = test_suites.split() | ||
| 186 | |||
| 182 | if args.run_tests: | 187 | if args.run_tests: |
| 183 | self.tc_kwargs['load']['modules'] = args.run_tests | 188 | self.tc_kwargs['load']['modules'] = args.run_tests |
| 184 | self.tc_kwargs['load']['modules_required'] = args.run_tests | 189 | self.tc_kwargs['load']['modules_required'] = args.run_tests |
| 190 | elif test_suites: | ||
| 191 | self.tc_kwargs['load']['modules'] = test_suites | ||
| 185 | else: | 192 | else: |
| 186 | self.tc_kwargs['load']['modules'] = [] | 193 | self.tc_kwargs['load']['modules'] = [] |
| 187 | 194 | ||
diff --git a/meta/lib/oeqa/core/decorator/__init__.py b/meta/lib/oeqa/core/decorator/__init__.py index 1a82518ab6..93efd30e1d 100644 --- a/meta/lib/oeqa/core/decorator/__init__.py +++ b/meta/lib/oeqa/core/decorator/__init__.py | |||
| @@ -5,8 +5,7 @@ | |||
| 5 | # | 5 | # |
| 6 | 6 | ||
| 7 | from functools import wraps | 7 | from functools import wraps |
| 8 | from abc import abstractmethod, ABCMeta | 8 | from abc import ABCMeta |
| 9 | from oeqa.core.utils.misc import strToList | ||
| 10 | 9 | ||
| 11 | decoratorClasses = set() | 10 | decoratorClasses = set() |
| 12 | 11 | ||
| @@ -65,15 +64,11 @@ class OETestDiscover(OETestDecorator): | |||
| 65 | return registry['cases'] | 64 | return registry['cases'] |
| 66 | 65 | ||
| 67 | def OETestTag(*tags): | 66 | def OETestTag(*tags): |
| 68 | expandedtags = [] | ||
| 69 | for tag in tags: | ||
| 70 | expandedtags += strToList(tag) | ||
| 71 | def decorator(item): | 67 | def decorator(item): |
| 72 | if hasattr(item, "__oeqa_testtags"): | 68 | if hasattr(item, "__oeqa_testtags"): |
| 73 | # do not append, create a new list (to handle classes with inheritance) | 69 | # do not append, create a new list (to handle classes with inheritance) |
| 74 | item.__oeqa_testtags = list(item.__oeqa_testtags) + expandedtags | 70 | item.__oeqa_testtags = list(item.__oeqa_testtags) + list(tags) |
| 75 | else: | 71 | else: |
| 76 | item.__oeqa_testtags = expandedtags | 72 | item.__oeqa_testtags = tags |
| 77 | return item | 73 | return item |
| 78 | return decorator | 74 | return decorator |
| 79 | |||
diff --git a/meta/lib/oeqa/core/decorator/data.py b/meta/lib/oeqa/core/decorator/data.py index bc4939e87c..0daf46334f 100644 --- a/meta/lib/oeqa/core/decorator/data.py +++ b/meta/lib/oeqa/core/decorator/data.py | |||
| @@ -13,8 +13,8 @@ def has_feature(td, feature): | |||
| 13 | Checks for feature in DISTRO_FEATURES or IMAGE_FEATURES. | 13 | Checks for feature in DISTRO_FEATURES or IMAGE_FEATURES. |
| 14 | """ | 14 | """ |
| 15 | 15 | ||
| 16 | if (feature in td.get('DISTRO_FEATURES', '') or | 16 | if (feature in td.get('DISTRO_FEATURES', '').split() or |
| 17 | feature in td.get('IMAGE_FEATURES', '')): | 17 | feature in td.get('IMAGE_FEATURES', '').split()): |
| 18 | return True | 18 | return True |
| 19 | return False | 19 | return False |
| 20 | 20 | ||
| @@ -23,18 +23,7 @@ def has_machine(td, machine): | |||
| 23 | Checks for MACHINE. | 23 | Checks for MACHINE. |
| 24 | """ | 24 | """ |
| 25 | 25 | ||
| 26 | if (machine in td.get('MACHINE', '')): | 26 | if (machine == td.get('MACHINE', '')): |
| 27 | return True | ||
| 28 | return False | ||
| 29 | |||
| 30 | def is_qemu(td, qemu): | ||
| 31 | """ | ||
| 32 | Checks if MACHINE is qemu. | ||
| 33 | """ | ||
| 34 | |||
| 35 | machine = td.get('MACHINE', '') | ||
| 36 | if (qemu in td.get('MACHINE', '') or | ||
| 37 | machine.startswith('qemu')): | ||
| 38 | return True | 27 | return True |
| 39 | return False | 28 | return False |
| 40 | 29 | ||
| @@ -189,34 +178,65 @@ class skipIfMachine(OETestDecorator): | |||
| 189 | @registerDecorator | 178 | @registerDecorator |
| 190 | class skipIfNotQemu(OETestDecorator): | 179 | class skipIfNotQemu(OETestDecorator): |
| 191 | """ | 180 | """ |
| 192 | Skip test based on MACHINE. | 181 | Skip test if MACHINE is not qemu* |
| 193 | |||
| 194 | value must be a qemu MACHINE or it will skip the test | ||
| 195 | with msg as the reason. | ||
| 196 | """ | 182 | """ |
| 183 | def setUpDecorator(self): | ||
| 184 | self.logger.debug("Checking if not qemu MACHINE") | ||
| 185 | if not self.case.td.get('MACHINE', '').startswith('qemu'): | ||
| 186 | self.case.skipTest('Test only runs on qemu machines') | ||
| 197 | 187 | ||
| 198 | attrs = ('value', 'msg') | 188 | @registerDecorator |
| 199 | 189 | class skipIfNotQemuUsermode(OETestDecorator): | |
| 190 | """ | ||
| 191 | Skip test if MACHINE_FEATURES does not contain qemu-usermode | ||
| 192 | """ | ||
| 200 | def setUpDecorator(self): | 193 | def setUpDecorator(self): |
| 201 | msg = ('Checking if %s is not this MACHINE' % self.value) | 194 | self.logger.debug("Checking if MACHINE_FEATURES does not contain qemu-usermode") |
| 202 | self.logger.debug(msg) | 195 | if 'qemu-usermode' not in self.case.td.get('MACHINE_FEATURES', '').split(): |
| 203 | if not is_qemu(self.case.td, self.value): | 196 | self.case.skipTest('Test requires qemu-usermode in MACHINE_FEATURES') |
| 204 | self.case.skipTest(self.msg) | ||
| 205 | 197 | ||
| 206 | @registerDecorator | 198 | @registerDecorator |
| 207 | class skipIfQemu(OETestDecorator): | 199 | class skipIfQemu(OETestDecorator): |
| 208 | """ | 200 | """ |
| 209 | Skip test based on Qemu Machine. | 201 | Skip test if MACHINE is qemu* |
| 202 | """ | ||
| 203 | def setUpDecorator(self): | ||
| 204 | self.logger.debug("Checking if qemu MACHINE") | ||
| 205 | if self.case.td.get('MACHINE', '').startswith('qemu'): | ||
| 206 | self.case.skipTest('Test only runs on real hardware') | ||
| 210 | 207 | ||
| 211 | value must not be a qemu machine or it will skip the test | 208 | @registerDecorator |
| 212 | with msg as the reason. | 209 | class skipIfArch(OETestDecorator): |
| 213 | """ | 210 | """ |
| 211 | Skip test if HOST_ARCH is present in the tuple specified. | ||
| 212 | """ | ||
| 214 | 213 | ||
| 215 | attrs = ('value', 'msg') | 214 | attrs = ('archs',) |
| 215 | def setUpDecorator(self): | ||
| 216 | arch = self.case.td['HOST_ARCH'] | ||
| 217 | if arch in self.archs: | ||
| 218 | self.case.skipTest('Test skipped on %s' % arch) | ||
| 219 | |||
| 220 | @registerDecorator | ||
| 221 | class skipIfNotArch(OETestDecorator): | ||
| 222 | """ | ||
| 223 | Skip test if HOST_ARCH is not present in the tuple specified. | ||
| 224 | """ | ||
| 216 | 225 | ||
| 226 | attrs = ('archs',) | ||
| 217 | def setUpDecorator(self): | 227 | def setUpDecorator(self): |
| 218 | msg = ('Checking if %s is this MACHINE' % self.value) | 228 | arch = self.case.td['HOST_ARCH'] |
| 219 | self.logger.debug(msg) | 229 | if arch not in self.archs: |
| 220 | if is_qemu(self.case.td, self.value): | 230 | self.case.skipTest('Test skipped on %s' % arch) |
| 221 | self.case.skipTest(self.msg) | ||
| 222 | 231 | ||
| 232 | @registerDecorator | ||
| 233 | class skipIfNotBuildArch(OETestDecorator): | ||
| 234 | """ | ||
| 235 | Skip test if BUILD_ARCH is not present in the tuple specified. | ||
| 236 | """ | ||
| 237 | |||
| 238 | attrs = ('archs',) | ||
| 239 | def setUpDecorator(self): | ||
| 240 | arch = self.case.td['BUILD_ARCH'] | ||
| 241 | if arch not in self.archs: | ||
| 242 | self.case.skipTest('Test skipped on %s' % arch) | ||
diff --git a/meta/lib/oeqa/core/decorator/oetimeout.py b/meta/lib/oeqa/core/decorator/oetimeout.py index df90d1c798..5e6873ad48 100644 --- a/meta/lib/oeqa/core/decorator/oetimeout.py +++ b/meta/lib/oeqa/core/decorator/oetimeout.py | |||
| @@ -24,5 +24,6 @@ class OETimeout(OETestDecorator): | |||
| 24 | 24 | ||
| 25 | def tearDownDecorator(self): | 25 | def tearDownDecorator(self): |
| 26 | signal.alarm(0) | 26 | signal.alarm(0) |
| 27 | signal.signal(signal.SIGALRM, self.alarmSignal) | 27 | if hasattr(self, 'alarmSignal'): |
| 28 | self.logger.debug("Removed SIGALRM handler") | 28 | signal.signal(signal.SIGALRM, self.alarmSignal) |
| 29 | self.logger.debug("Removed SIGALRM handler") | ||
diff --git a/meta/lib/oeqa/core/loader.py b/meta/lib/oeqa/core/loader.py index 11978213b8..d12d5a055c 100644 --- a/meta/lib/oeqa/core/loader.py +++ b/meta/lib/oeqa/core/loader.py | |||
| @@ -37,7 +37,7 @@ def _find_duplicated_modules(suite, directory): | |||
| 37 | if path: | 37 | if path: |
| 38 | raise ImportError("Duplicated %s module found in %s" % (module, path)) | 38 | raise ImportError("Duplicated %s module found in %s" % (module, path)) |
| 39 | 39 | ||
| 40 | def _built_modules_dict(modules): | 40 | def _built_modules_dict(modules, logger): |
| 41 | modules_dict = {} | 41 | modules_dict = {} |
| 42 | 42 | ||
| 43 | if modules == None: | 43 | if modules == None: |
| @@ -48,6 +48,9 @@ def _built_modules_dict(modules): | |||
| 48 | # characters, whereas class names do | 48 | # characters, whereas class names do |
| 49 | m = re.match(r'^([0-9a-z_.]+)(?:\.(\w[^.]*)(?:\.([^.]+))?)?$', module, flags=re.ASCII) | 49 | m = re.match(r'^([0-9a-z_.]+)(?:\.(\w[^.]*)(?:\.([^.]+))?)?$', module, flags=re.ASCII) |
| 50 | if not m: | 50 | if not m: |
| 51 | logger.warn("module '%s' was skipped from selected modules, "\ | ||
| 52 | "because it doesn't match with module name assumptions: "\ | ||
| 53 | "package and module names do not contain upper case characters, whereas class names do" % module) | ||
| 51 | continue | 54 | continue |
| 52 | 55 | ||
| 53 | module_name, class_name, test_name = m.groups() | 56 | module_name, class_name, test_name = m.groups() |
| @@ -58,6 +61,8 @@ def _built_modules_dict(modules): | |||
| 58 | modules_dict[module_name][class_name] = [] | 61 | modules_dict[module_name][class_name] = [] |
| 59 | if test_name and test_name not in modules_dict[module_name][class_name]: | 62 | if test_name and test_name not in modules_dict[module_name][class_name]: |
| 60 | modules_dict[module_name][class_name].append(test_name) | 63 | modules_dict[module_name][class_name].append(test_name) |
| 64 | if modules and not modules_dict: | ||
| 65 | raise OEQATestNotFound("All selected modules were skipped, this would trigger selftest with all tests and -r ignored.") | ||
| 61 | 66 | ||
| 62 | return modules_dict | 67 | return modules_dict |
| 63 | 68 | ||
| @@ -71,7 +76,7 @@ class OETestLoader(unittest.TestLoader): | |||
| 71 | *args, **kwargs): | 76 | *args, **kwargs): |
| 72 | self.tc = tc | 77 | self.tc = tc |
| 73 | 78 | ||
| 74 | self.modules = _built_modules_dict(modules) | 79 | self.modules = _built_modules_dict(modules, tc.logger) |
| 75 | 80 | ||
| 76 | self.tests = tests | 81 | self.tests = tests |
| 77 | self.modules_required = modules_required | 82 | self.modules_required = modules_required |
| @@ -311,6 +316,9 @@ class OETestLoader(unittest.TestLoader): | |||
| 311 | module_name_small in self.modules) \ | 316 | module_name_small in self.modules) \ |
| 312 | else False | 317 | else False |
| 313 | 318 | ||
| 319 | if any(c.isupper() for c in module.__name__): | ||
| 320 | raise SystemExit("Module '%s' contains uppercase characters and this isn't supported. Please fix the module name." % module.__name__) | ||
| 321 | |||
| 314 | return (load_module, load_underscore) | 322 | return (load_module, load_underscore) |
| 315 | 323 | ||
| 316 | 324 | ||
diff --git a/meta/lib/oeqa/core/runner.py b/meta/lib/oeqa/core/runner.py index d50690ab37..b683d9b80a 100644 --- a/meta/lib/oeqa/core/runner.py +++ b/meta/lib/oeqa/core/runner.py | |||
| @@ -44,6 +44,7 @@ class OETestResult(_TestResult): | |||
| 44 | self.endtime = {} | 44 | self.endtime = {} |
| 45 | self.progressinfo = {} | 45 | self.progressinfo = {} |
| 46 | self.extraresults = {} | 46 | self.extraresults = {} |
| 47 | self.shownmsg = [] | ||
| 47 | 48 | ||
| 48 | # Inject into tc so that TestDepends decorator can see results | 49 | # Inject into tc so that TestDepends decorator can see results |
| 49 | tc.results = self | 50 | tc.results = self |
| @@ -74,6 +75,7 @@ class OETestResult(_TestResult): | |||
| 74 | for (scase, msg) in getattr(self, t): | 75 | for (scase, msg) in getattr(self, t): |
| 75 | if test.id() == scase.id(): | 76 | if test.id() == scase.id(): |
| 76 | self.tc.logger.info(str(msg)) | 77 | self.tc.logger.info(str(msg)) |
| 78 | self.shownmsg.append(test.id()) | ||
| 77 | break | 79 | break |
| 78 | 80 | ||
| 79 | def logSummary(self, component, context_msg=''): | 81 | def logSummary(self, component, context_msg=''): |
| @@ -169,7 +171,6 @@ class OETestResult(_TestResult): | |||
| 169 | 171 | ||
| 170 | def logDetails(self, json_file_dir=None, configuration=None, result_id=None, | 172 | def logDetails(self, json_file_dir=None, configuration=None, result_id=None, |
| 171 | dump_streams=False): | 173 | dump_streams=False): |
| 172 | self.tc.logger.info("RESULTS:") | ||
| 173 | 174 | ||
| 174 | result = self.extraresults | 175 | result = self.extraresults |
| 175 | logs = {} | 176 | logs = {} |
| @@ -193,6 +194,10 @@ class OETestResult(_TestResult): | |||
| 193 | report = {'status': status} | 194 | report = {'status': status} |
| 194 | if log: | 195 | if log: |
| 195 | report['log'] = log | 196 | report['log'] = log |
| 197 | # Class setup failures wouldn't enter stopTest so would never display | ||
| 198 | if case.id() not in self.shownmsg: | ||
| 199 | self.tc.logger.info("Failure (%s) for %s:\n" % (status, case.id()) + log) | ||
| 200 | |||
| 196 | if duration: | 201 | if duration: |
| 197 | report['duration'] = duration | 202 | report['duration'] = duration |
| 198 | 203 | ||
| @@ -215,6 +220,7 @@ class OETestResult(_TestResult): | |||
| 215 | report['stderr'] = stderr | 220 | report['stderr'] = stderr |
| 216 | result[case.id()] = report | 221 | result[case.id()] = report |
| 217 | 222 | ||
| 223 | self.tc.logger.info("RESULTS:") | ||
| 218 | for i in ['PASSED', 'SKIPPED', 'EXPECTEDFAIL', 'ERROR', 'FAILED', 'UNKNOWN']: | 224 | for i in ['PASSED', 'SKIPPED', 'EXPECTEDFAIL', 'ERROR', 'FAILED', 'UNKNOWN']: |
| 219 | if i not in logs: | 225 | if i not in logs: |
| 220 | continue | 226 | continue |
| @@ -229,6 +235,10 @@ class OETestResult(_TestResult): | |||
| 229 | # Override as we unexpected successes aren't failures for us | 235 | # Override as we unexpected successes aren't failures for us |
| 230 | return (len(self.failures) == len(self.errors) == 0) | 236 | return (len(self.failures) == len(self.errors) == 0) |
| 231 | 237 | ||
| 238 | def hasAnyFailingTest(self): | ||
| 239 | # Account for expected failures | ||
| 240 | return not self.wasSuccessful() or len(self.expectedFailures) | ||
| 241 | |||
| 232 | class OEListTestsResult(object): | 242 | class OEListTestsResult(object): |
| 233 | def wasSuccessful(self): | 243 | def wasSuccessful(self): |
| 234 | return True | 244 | return True |
| @@ -347,7 +357,7 @@ class OETestResultJSONHelper(object): | |||
| 347 | os.makedirs(write_dir, exist_ok=True) | 357 | os.makedirs(write_dir, exist_ok=True) |
| 348 | test_results = self._get_existing_testresults_if_available(write_dir) | 358 | test_results = self._get_existing_testresults_if_available(write_dir) |
| 349 | test_results[result_id] = {'configuration': configuration, 'result': test_result} | 359 | test_results[result_id] = {'configuration': configuration, 'result': test_result} |
| 350 | json_testresults = json.dumps(test_results, sort_keys=True, indent=4) | 360 | json_testresults = json.dumps(test_results, sort_keys=True, indent=1) |
| 351 | self._write_file(write_dir, self.testresult_filename, json_testresults) | 361 | self._write_file(write_dir, self.testresult_filename, json_testresults) |
| 352 | if has_bb: | 362 | if has_bb: |
| 353 | bb.utils.unlockfile(lf) | 363 | bb.utils.unlockfile(lf) |
diff --git a/meta/lib/oeqa/core/target/__init__.py b/meta/lib/oeqa/core/target/__init__.py index 1382aa9b52..177f648fe3 100644 --- a/meta/lib/oeqa/core/target/__init__.py +++ b/meta/lib/oeqa/core/target/__init__.py | |||
| @@ -10,6 +10,7 @@ class OETarget(object): | |||
| 10 | 10 | ||
| 11 | def __init__(self, logger, *args, **kwargs): | 11 | def __init__(self, logger, *args, **kwargs): |
| 12 | self.logger = logger | 12 | self.logger = logger |
| 13 | self.runner = None | ||
| 13 | 14 | ||
| 14 | @abstractmethod | 15 | @abstractmethod |
| 15 | def start(self): | 16 | def start(self): |
diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py index 0f29414df5..769a6fec7e 100644 --- a/meta/lib/oeqa/core/target/qemu.py +++ b/meta/lib/oeqa/core/target/qemu.py | |||
| @@ -8,20 +8,21 @@ import os | |||
| 8 | import sys | 8 | import sys |
| 9 | import signal | 9 | import signal |
| 10 | import time | 10 | import time |
| 11 | import glob | ||
| 12 | import subprocess | ||
| 11 | from collections import defaultdict | 13 | from collections import defaultdict |
| 12 | 14 | ||
| 13 | from .ssh import OESSHTarget | 15 | from .ssh import OESSHTarget |
| 14 | from oeqa.utils.qemurunner import QemuRunner | 16 | from oeqa.utils.qemurunner import QemuRunner |
| 15 | from oeqa.utils.dump import TargetDumper | ||
| 16 | 17 | ||
| 17 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] | 18 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic', 'wic.zst', 'ext3.zst', 'ext4.zst'] |
| 18 | 19 | ||
| 19 | class OEQemuTarget(OESSHTarget): | 20 | class OEQemuTarget(OESSHTarget): |
| 20 | def __init__(self, logger, server_ip, timeout=300, user='root', | 21 | def __init__(self, logger, server_ip, timeout=300, user='root', |
| 21 | port=None, machine='', rootfs='', kernel='', kvm=False, slirp=False, | 22 | port=None, machine='', rootfs='', kernel='', kvm=False, slirp=False, |
| 22 | dump_dir='', dump_host_cmds='', display='', bootlog='', | 23 | dump_dir='', display='', bootlog='', |
| 23 | tmpdir='', dir_image='', boottime=60, serial_ports=2, | 24 | tmpdir='', dir_image='', boottime=60, serial_ports=2, |
| 24 | boot_patterns = defaultdict(str), ovmf=False, **kwargs): | 25 | boot_patterns = defaultdict(str), ovmf=False, tmpfsdir=None, **kwargs): |
| 25 | 26 | ||
| 26 | super(OEQemuTarget, self).__init__(logger, None, server_ip, timeout, | 27 | super(OEQemuTarget, self).__init__(logger, None, server_ip, timeout, |
| 27 | user, port) | 28 | user, port) |
| @@ -35,17 +36,15 @@ class OEQemuTarget(OESSHTarget): | |||
| 35 | self.ovmf = ovmf | 36 | self.ovmf = ovmf |
| 36 | self.use_slirp = slirp | 37 | self.use_slirp = slirp |
| 37 | self.boot_patterns = boot_patterns | 38 | self.boot_patterns = boot_patterns |
| 39 | self.dump_dir = dump_dir | ||
| 40 | self.bootlog = bootlog | ||
| 38 | 41 | ||
| 39 | self.runner = QemuRunner(machine=machine, rootfs=rootfs, tmpdir=tmpdir, | 42 | self.runner = QemuRunner(machine=machine, rootfs=rootfs, tmpdir=tmpdir, |
| 40 | deploy_dir_image=dir_image, display=display, | 43 | deploy_dir_image=dir_image, display=display, |
| 41 | logfile=bootlog, boottime=boottime, | 44 | logfile=bootlog, boottime=boottime, |
| 42 | use_kvm=kvm, use_slirp=slirp, dump_dir=dump_dir, | 45 | use_kvm=kvm, use_slirp=slirp, dump_dir=dump_dir, logger=logger, |
| 43 | dump_host_cmds=dump_host_cmds, logger=logger, | ||
| 44 | serial_ports=serial_ports, boot_patterns = boot_patterns, | 46 | serial_ports=serial_ports, boot_patterns = boot_patterns, |
| 45 | use_ovmf=ovmf) | 47 | use_ovmf=ovmf, tmpfsdir=tmpfsdir) |
| 46 | dump_target_cmds = kwargs.get("testimage_dump_target") | ||
| 47 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) | ||
| 48 | self.target_dumper.create_dir("qemu") | ||
| 49 | 48 | ||
| 50 | def start(self, params=None, extra_bootparams=None, runqemuparams=''): | 49 | def start(self, params=None, extra_bootparams=None, runqemuparams=''): |
| 51 | if self.use_slirp and not self.server_ip: | 50 | if self.use_slirp and not self.server_ip: |
| @@ -68,7 +67,28 @@ class OEQemuTarget(OESSHTarget): | |||
| 68 | self.server_ip = self.runner.server_ip | 67 | self.server_ip = self.runner.server_ip |
| 69 | else: | 68 | else: |
| 70 | self.stop() | 69 | self.stop() |
| 71 | raise RuntimeError("FAILED to start qemu - check the task log and the boot log") | 70 | # Display the first 20 lines of top and |
| 71 | # last 20 lines of the bootlog when the | ||
| 72 | # target is not being booted up. | ||
| 73 | topfile = glob.glob(self.dump_dir + "/*_qemu/host_*_top") | ||
| 74 | msg = "\n\n===== start: snippet =====\n\n" | ||
| 75 | for f in topfile: | ||
| 76 | msg += "file: %s\n\n" % f | ||
| 77 | with open(f) as tf: | ||
| 78 | for x in range(20): | ||
| 79 | msg += next(tf) | ||
| 80 | msg += "\n\n===== end: snippet =====\n\n" | ||
| 81 | blcmd = ["tail", "-20", self.bootlog] | ||
| 82 | msg += "===== start: snippet =====\n\n" | ||
| 83 | try: | ||
| 84 | out = subprocess.check_output(blcmd, stderr=subprocess.STDOUT, timeout=1).decode('utf-8') | ||
| 85 | msg += "file: %s\n\n" % self.bootlog | ||
| 86 | msg += out | ||
| 87 | except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as err: | ||
| 88 | msg += "Error running command: %s\n%s\n" % (blcmd, err) | ||
| 89 | msg += "\n\n===== end: snippet =====\n" | ||
| 90 | |||
| 91 | raise RuntimeError("FAILED to start qemu - check the task log and the boot log %s" % (msg)) | ||
| 72 | 92 | ||
| 73 | def stop(self): | 93 | def stop(self): |
| 74 | self.runner.stop() | 94 | self.runner.stop() |
diff --git a/meta/lib/oeqa/core/target/serial.py b/meta/lib/oeqa/core/target/serial.py new file mode 100644 index 0000000000..7c2cd8b248 --- /dev/null +++ b/meta/lib/oeqa/core/target/serial.py | |||
| @@ -0,0 +1,315 @@ | |||
| 1 | # | ||
| 2 | # SPDX-License-Identifier: MIT | ||
| 3 | # | ||
| 4 | |||
| 5 | import base64 | ||
| 6 | import logging | ||
| 7 | import os | ||
| 8 | from threading import Lock | ||
| 9 | from . import OETarget | ||
| 10 | |||
| 11 | class OESerialTarget(OETarget): | ||
| 12 | |||
| 13 | def __init__(self, logger, target_ip, server_ip, server_port=0, | ||
| 14 | timeout=300, serialcontrol_cmd=None, serialcontrol_extra_args=None, | ||
| 15 | serialcontrol_ps1=None, serialcontrol_connect_timeout=None, | ||
| 16 | machine=None, **kwargs): | ||
| 17 | if not logger: | ||
| 18 | logger = logging.getLogger('target') | ||
| 19 | logger.setLevel(logging.INFO) | ||
| 20 | filePath = os.path.join(os.getcwd(), 'remoteTarget.log') | ||
| 21 | fileHandler = logging.FileHandler(filePath, 'w', 'utf-8') | ||
| 22 | formatter = logging.Formatter( | ||
| 23 | '%(asctime)s.%(msecs)03d %(levelname)s: %(message)s', | ||
| 24 | '%H:%M:%S') | ||
| 25 | fileHandler.setFormatter(formatter) | ||
| 26 | logger.addHandler(fileHandler) | ||
| 27 | |||
| 28 | super(OESerialTarget, self).__init__(logger) | ||
| 29 | |||
| 30 | if serialcontrol_ps1: | ||
| 31 | self.target_ps1 = serialcontrol_ps1 | ||
| 32 | elif machine: | ||
| 33 | # fallback to a default value which assumes root@machine | ||
| 34 | self.target_ps1 = f'root@{machine}:.*# ' | ||
| 35 | else: | ||
| 36 | raise ValueError("Unable to determine shell command prompt (PS1) format.") | ||
| 37 | |||
| 38 | if not serialcontrol_cmd: | ||
| 39 | raise ValueError("Unable to determine serial control command.") | ||
| 40 | |||
| 41 | if serialcontrol_extra_args: | ||
| 42 | self.connection_script = f'{serialcontrol_cmd} {serialcontrol_extra_args}' | ||
| 43 | else: | ||
| 44 | self.connection_script = serialcontrol_cmd | ||
| 45 | |||
| 46 | if serialcontrol_connect_timeout: | ||
| 47 | self.connect_timeout = serialcontrol_connect_timeout | ||
| 48 | else: | ||
| 49 | self.connect_timeout = 10 # default to 10s connection timeout | ||
| 50 | |||
| 51 | self.default_command_timeout = timeout | ||
| 52 | self.ip = target_ip | ||
| 53 | self.server_ip = server_ip | ||
| 54 | self.server_port = server_port | ||
| 55 | self.conn = None | ||
| 56 | self.mutex = Lock() | ||
| 57 | |||
| 58 | def start(self, **kwargs): | ||
| 59 | pass | ||
| 60 | |||
| 61 | def stop(self, **kwargs): | ||
| 62 | pass | ||
| 63 | |||
| 64 | def get_connection(self): | ||
| 65 | if self.conn is None: | ||
| 66 | self.conn = SerialConnection(self.connection_script, | ||
| 67 | self.target_ps1, | ||
| 68 | self.connect_timeout, | ||
| 69 | self.default_command_timeout) | ||
| 70 | |||
| 71 | return self.conn | ||
| 72 | |||
| 73 | def run(self, cmd, timeout=None): | ||
| 74 | """ | ||
| 75 | Runs command on target over the provided serial connection. | ||
| 76 | The first call will open the connection, and subsequent | ||
| 77 | calls will re-use the same connection to send new commands. | ||
| 78 | |||
| 79 | command: Command to run on target. | ||
| 80 | timeout: <value>: Kill command after <val> seconds. | ||
| 81 | None: Kill command default value seconds. | ||
| 82 | 0: No timeout, runs until return. | ||
| 83 | """ | ||
| 84 | # Lock needed to avoid multiple threads running commands concurrently | ||
| 85 | # A serial connection can only be used by one caller at a time | ||
| 86 | with self.mutex: | ||
| 87 | conn = self.get_connection() | ||
| 88 | |||
| 89 | self.logger.debug(f"[Running]$ {cmd}") | ||
| 90 | # Run the command, then echo $? to get the command's return code | ||
| 91 | try: | ||
| 92 | output = conn.run_command(cmd, timeout) | ||
| 93 | status = conn.run_command("echo $?") | ||
| 94 | self.logger.debug(f" [stdout]: {output}") | ||
| 95 | self.logger.debug(f" [ret code]: {status}\n\n") | ||
| 96 | except SerialTimeoutException as e: | ||
| 97 | self.logger.debug(e) | ||
| 98 | output = "" | ||
| 99 | status = 255 | ||
| 100 | |||
| 101 | # Return to $HOME after each command to simulate a stateless SSH connection | ||
| 102 | conn.run_command('cd "$HOME"') | ||
| 103 | |||
| 104 | return (int(status), output) | ||
| 105 | |||
| 106 | def copyTo(self, localSrc, remoteDst): | ||
| 107 | """ | ||
| 108 | Copies files by converting them to base 32, then transferring | ||
| 109 | the ASCII text to the target, and decoding it in place on the | ||
| 110 | target. | ||
| 111 | |||
| 112 | On a 115k baud serial connection, this method transfers at | ||
| 113 | roughly 30kbps. | ||
| 114 | """ | ||
| 115 | with open(localSrc, 'rb') as file: | ||
| 116 | data = file.read() | ||
| 117 | |||
| 118 | b32 = base64.b32encode(data).decode('utf-8') | ||
| 119 | |||
| 120 | # To avoid shell line limits, send a chunk at a time | ||
| 121 | SPLIT_LEN = 512 | ||
| 122 | lines = [b32[i:i+SPLIT_LEN] for i in range(0, len(b32), SPLIT_LEN)] | ||
| 123 | |||
| 124 | with self.mutex: | ||
| 125 | conn = self.get_connection() | ||
| 126 | |||
| 127 | filename = os.path.basename(localSrc) | ||
| 128 | TEMP = f'/tmp/{filename}.b32' | ||
| 129 | |||
| 130 | # Create or empty out the temp file | ||
| 131 | conn.run_command(f'echo -n "" > {TEMP}') | ||
| 132 | |||
| 133 | for line in lines: | ||
| 134 | conn.run_command(f'echo -n {line} >> {TEMP}') | ||
| 135 | |||
| 136 | # Check to see whether the remoteDst is a directory | ||
| 137 | is_directory = conn.run_command(f'[[ -d {remoteDst} ]]; echo $?') | ||
| 138 | if int(is_directory) == 0: | ||
| 139 | # append the localSrc filename to the end of remoteDst | ||
| 140 | remoteDst = os.path.join(remoteDst, filename) | ||
| 141 | |||
| 142 | conn.run_command(f'base32 -d {TEMP} > {remoteDst}') | ||
| 143 | conn.run_command(f'rm {TEMP}') | ||
| 144 | |||
| 145 | return 0, 'Success' | ||
| 146 | |||
| 147 | def copyFrom(self, remoteSrc, localDst): | ||
| 148 | """ | ||
| 149 | Copies files by converting them to base 32 on the target, then | ||
| 150 | transferring the ASCII text to the host. That text is then | ||
| 151 | decoded here and written out to the destination. | ||
| 152 | |||
| 153 | On a 115k baud serial connection, this method transfers at | ||
| 154 | roughly 30kbps. | ||
| 155 | """ | ||
| 156 | with self.mutex: | ||
| 157 | b32 = self.get_connection().run_command(f'base32 {remoteSrc}') | ||
| 158 | |||
| 159 | data = base64.b32decode(b32.replace('\r\n', '')) | ||
| 160 | |||
| 161 | # If the local path is a directory, get the filename from | ||
| 162 | # the remoteSrc path and append it to localDst | ||
| 163 | if os.path.isdir(localDst): | ||
| 164 | filename = os.path.basename(remoteSrc) | ||
| 165 | localDst = os.path.join(localDst, filename) | ||
| 166 | |||
| 167 | with open(localDst, 'wb') as file: | ||
| 168 | file.write(data) | ||
| 169 | |||
| 170 | return 0, 'Success' | ||
| 171 | |||
| 172 | def copyDirTo(self, localSrc, remoteDst): | ||
| 173 | """ | ||
| 174 | Copy recursively localSrc directory to remoteDst in target. | ||
| 175 | """ | ||
| 176 | |||
| 177 | for root, dirs, files in os.walk(localSrc): | ||
| 178 | # Create directories in the target as needed | ||
| 179 | for d in dirs: | ||
| 180 | tmpDir = os.path.join(root, d).replace(localSrc, "") | ||
| 181 | newDir = os.path.join(remoteDst, tmpDir.lstrip("/")) | ||
| 182 | cmd = "mkdir -p %s" % newDir | ||
| 183 | self.run(cmd) | ||
| 184 | |||
| 185 | # Copy files into the target | ||
| 186 | for f in files: | ||
| 187 | tmpFile = os.path.join(root, f).replace(localSrc, "") | ||
| 188 | dstFile = os.path.join(remoteDst, tmpFile.lstrip("/")) | ||
| 189 | srcFile = os.path.join(root, f) | ||
| 190 | self.copyTo(srcFile, dstFile) | ||
| 191 | |||
| 192 | def deleteFiles(self, remotePath, files): | ||
| 193 | """ | ||
| 194 | Deletes files in target's remotePath. | ||
| 195 | """ | ||
| 196 | |||
| 197 | cmd = "rm" | ||
| 198 | if not isinstance(files, list): | ||
| 199 | files = [files] | ||
| 200 | |||
| 201 | for f in files: | ||
| 202 | cmd = "%s %s" % (cmd, os.path.join(remotePath, f)) | ||
| 203 | |||
| 204 | self.run(cmd) | ||
| 205 | |||
| 206 | def deleteDir(self, remotePath): | ||
| 207 | """ | ||
| 208 | Deletes target's remotePath directory. | ||
| 209 | """ | ||
| 210 | |||
| 211 | cmd = "rmdir %s" % remotePath | ||
| 212 | self.run(cmd) | ||
| 213 | |||
| 214 | def deleteDirStructure(self, localPath, remotePath): | ||
| 215 | """ | ||
| 216 | Delete recursively localPath structure directory in target's remotePath. | ||
| 217 | |||
| 218 | This function is useful to delete a package that is installed in the | ||
| 219 | device under test (DUT) and the host running the test has such package | ||
| 220 | extracted in tmp directory. | ||
| 221 | |||
| 222 | Example: | ||
| 223 | pwd: /home/user/tmp | ||
| 224 | tree: . | ||
| 225 | └── work | ||
| 226 | ├── dir1 | ||
| 227 | │  └── file1 | ||
| 228 | └── dir2 | ||
| 229 | |||
| 230 | localpath = "/home/user/tmp" and remotepath = "/home/user" | ||
| 231 | |||
| 232 | With the above variables this function will try to delete the | ||
| 233 | directory in the DUT in this order: | ||
| 234 | /home/user/work/dir1/file1 | ||
| 235 | /home/user/work/dir1 (if dir is empty) | ||
| 236 | /home/user/work/dir2 (if dir is empty) | ||
| 237 | /home/user/work (if dir is empty) | ||
| 238 | """ | ||
| 239 | |||
| 240 | for root, dirs, files in os.walk(localPath, topdown=False): | ||
| 241 | # Delete files first | ||
| 242 | tmpDir = os.path.join(root).replace(localPath, "") | ||
| 243 | remoteDir = os.path.join(remotePath, tmpDir.lstrip("/")) | ||
| 244 | self.deleteFiles(remoteDir, files) | ||
| 245 | |||
| 246 | # Remove dirs if empty | ||
| 247 | for d in dirs: | ||
| 248 | tmpDir = os.path.join(root, d).replace(localPath, "") | ||
| 249 | remoteDir = os.path.join(remotePath, tmpDir.lstrip("/")) | ||
| 250 | self.deleteDir(remoteDir) | ||
| 251 | |||
| 252 | class SerialTimeoutException(Exception): | ||
| 253 | def __init__(self, msg): | ||
| 254 | self.msg = msg | ||
| 255 | def __str__(self): | ||
| 256 | return self.msg | ||
| 257 | |||
| 258 | class SerialConnection: | ||
| 259 | |||
| 260 | def __init__(self, script, target_prompt, connect_timeout, default_command_timeout): | ||
| 261 | import pexpect # limiting scope to avoid build dependency | ||
| 262 | self.prompt = target_prompt | ||
| 263 | self.connect_timeout = connect_timeout | ||
| 264 | self.default_command_timeout = default_command_timeout | ||
| 265 | self.conn = pexpect.spawn('/bin/bash', ['-c', script], encoding='utf8') | ||
| 266 | self._seek_to_clean_shell() | ||
| 267 | # Disable echo to avoid the need to parse the outgoing command | ||
| 268 | self.run_command('stty -echo') | ||
| 269 | |||
| 270 | def _seek_to_clean_shell(self): | ||
| 271 | """ | ||
| 272 | Attempts to find a clean shell, meaning it is clear and | ||
| 273 | ready to accept a new command. This is necessary to ensure | ||
| 274 | the correct output is captured from each command. | ||
| 275 | """ | ||
| 276 | import pexpect # limiting scope to avoid build dependency | ||
| 277 | # Look for a clean shell | ||
| 278 | # Wait a short amount of time for the connection to finish | ||
| 279 | pexpect_code = self.conn.expect([self.prompt, pexpect.TIMEOUT], | ||
| 280 | timeout=self.connect_timeout) | ||
| 281 | |||
| 282 | # if a timeout occurred, send an empty line and wait for a clean shell | ||
| 283 | if pexpect_code == 1: | ||
| 284 | # send a newline to clear and present the shell | ||
| 285 | self.conn.sendline("") | ||
| 286 | pexpect_code = self.conn.expect(self.prompt) | ||
| 287 | |||
| 288 | def run_command(self, cmd, timeout=None): | ||
| 289 | """ | ||
| 290 | Runs command on target over the provided serial connection. | ||
| 291 | Returns any output on the shell while the command was run. | ||
| 292 | |||
| 293 | command: Command to run on target. | ||
| 294 | timeout: <value>: Kill command after <val> seconds. | ||
| 295 | None: Kill command default value seconds. | ||
| 296 | 0: No timeout, runs until return. | ||
| 297 | """ | ||
| 298 | import pexpect # limiting scope to avoid build dependency | ||
| 299 | # Convert from the OETarget defaults to pexpect timeout values | ||
| 300 | if timeout is None: | ||
| 301 | timeout = self.default_command_timeout | ||
| 302 | elif timeout == 0: | ||
| 303 | timeout = None # passing None to pexpect is infinite timeout | ||
| 304 | |||
| 305 | self.conn.sendline(cmd) | ||
| 306 | pexpect_code = self.conn.expect([self.prompt, pexpect.TIMEOUT], timeout=timeout) | ||
| 307 | |||
| 308 | # check for timeout | ||
| 309 | if pexpect_code == 1: | ||
| 310 | self.conn.send('\003') # send Ctrl+C | ||
| 311 | self._seek_to_clean_shell() | ||
| 312 | raise SerialTimeoutException(f'Timeout executing: {cmd} after {timeout}s') | ||
| 313 | |||
| 314 | return self.conn.before.removesuffix('\r\n') | ||
| 315 | |||
diff --git a/meta/lib/oeqa/core/target/ssh.py b/meta/lib/oeqa/core/target/ssh.py index 461448dbc5..0ac3ae4388 100644 --- a/meta/lib/oeqa/core/target/ssh.py +++ b/meta/lib/oeqa/core/target/ssh.py | |||
| @@ -34,12 +34,17 @@ class OESSHTarget(OETarget): | |||
| 34 | self.timeout = timeout | 34 | self.timeout = timeout |
| 35 | self.user = user | 35 | self.user = user |
| 36 | ssh_options = [ | 36 | ssh_options = [ |
| 37 | '-o', 'ServerAliveCountMax=2', | ||
| 38 | '-o', 'ServerAliveInterval=30', | ||
| 37 | '-o', 'UserKnownHostsFile=/dev/null', | 39 | '-o', 'UserKnownHostsFile=/dev/null', |
| 38 | '-o', 'StrictHostKeyChecking=no', | 40 | '-o', 'StrictHostKeyChecking=no', |
| 39 | '-o', 'LogLevel=ERROR' | 41 | '-o', 'LogLevel=ERROR' |
| 40 | ] | 42 | ] |
| 43 | scp_options = [ | ||
| 44 | '-r' | ||
| 45 | ] | ||
| 41 | self.ssh = ['ssh', '-l', self.user ] + ssh_options | 46 | self.ssh = ['ssh', '-l', self.user ] + ssh_options |
| 42 | self.scp = ['scp'] + ssh_options | 47 | self.scp = ['scp'] + ssh_options + scp_options |
| 43 | if port: | 48 | if port: |
| 44 | self.ssh = self.ssh + [ '-p', port ] | 49 | self.ssh = self.ssh + [ '-p', port ] |
| 45 | self.scp = self.scp + [ '-P', port ] | 50 | self.scp = self.scp + [ '-P', port ] |
| @@ -50,24 +55,28 @@ class OESSHTarget(OETarget): | |||
| 50 | def stop(self, **kwargs): | 55 | def stop(self, **kwargs): |
| 51 | pass | 56 | pass |
| 52 | 57 | ||
| 53 | def _run(self, command, timeout=None, ignore_status=True): | 58 | def _run(self, command, timeout=None, ignore_status=True, raw=False, ignore_ssh_fails=False): |
| 54 | """ | 59 | """ |
| 55 | Runs command in target using SSHProcess. | 60 | Runs command in target using SSHProcess. |
| 56 | """ | 61 | """ |
| 57 | self.logger.debug("[Running]$ %s" % " ".join(command)) | 62 | self.logger.debug("[Running]$ %s" % " ".join(command)) |
| 58 | 63 | ||
| 59 | starttime = time.time() | 64 | starttime = time.time() |
| 60 | status, output = SSHCall(command, self.logger, timeout) | 65 | status, output = SSHCall(command, self.logger, timeout, raw) |
| 61 | self.logger.debug("[Command returned '%d' after %.2f seconds]" | 66 | self.logger.debug("[Command returned '%d' after %.2f seconds]" |
| 62 | "" % (status, time.time() - starttime)) | 67 | "" % (status, time.time() - starttime)) |
| 63 | 68 | ||
| 64 | if status and not ignore_status: | 69 | if status == 255 and not ignore_ssh_fails: |
| 70 | raise AssertionError("ssh exited with status '255' for command " | ||
| 71 | "'%s': this is likely an SSH failure\n%s" | ||
| 72 | % (command, output)) | ||
| 73 | elif status and not ignore_status: | ||
| 65 | raise AssertionError("Command '%s' returned non-zero exit " | 74 | raise AssertionError("Command '%s' returned non-zero exit " |
| 66 | "status %d:\n%s" % (command, status, output)) | 75 | "status %d:\n%s" % (command, status, output)) |
| 67 | 76 | ||
| 68 | return (status, output) | 77 | return (status, output) |
| 69 | 78 | ||
| 70 | def run(self, command, timeout=None): | 79 | def run(self, command, timeout=None, ignore_status=True, raw=False, ignore_ssh_fails=False): |
| 71 | """ | 80 | """ |
| 72 | Runs command in target. | 81 | Runs command in target. |
| 73 | 82 | ||
| @@ -86,10 +95,12 @@ class OESSHTarget(OETarget): | |||
| 86 | else: | 95 | else: |
| 87 | processTimeout = self.timeout | 96 | processTimeout = self.timeout |
| 88 | 97 | ||
| 89 | status, output = self._run(sshCmd, processTimeout, True) | 98 | status, output = self._run(sshCmd, processTimeout, ignore_status, raw, ignore_ssh_fails) |
| 90 | self.logger.debug('Command: %s\nOutput: %s\n' % (command, output)) | 99 | if len(output) > (64 * 1024): |
| 91 | if (status == 255) and (('No route to host') in output): | 100 | self.logger.debug('Command: %s\nStatus: %d Output length: %s\n' % (command, status, len(output))) |
| 92 | self.target_dumper.dump_target() | 101 | else: |
| 102 | self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) | ||
| 103 | |||
| 93 | return (status, output) | 104 | return (status, output) |
| 94 | 105 | ||
| 95 | def copyTo(self, localSrc, remoteDst): | 106 | def copyTo(self, localSrc, remoteDst): |
| @@ -202,32 +213,51 @@ class OESSHTarget(OETarget): | |||
| 202 | remoteDir = os.path.join(remotePath, tmpDir.lstrip("/")) | 213 | remoteDir = os.path.join(remotePath, tmpDir.lstrip("/")) |
| 203 | self.deleteDir(remoteDir) | 214 | self.deleteDir(remoteDir) |
| 204 | 215 | ||
| 205 | def SSHCall(command, logger, timeout=None, **opts): | 216 | def SSHCall(command, logger, timeout=None, raw=False, **opts): |
| 206 | 217 | ||
| 207 | def run(): | 218 | def run(): |
| 208 | nonlocal output | 219 | nonlocal output |
| 209 | nonlocal process | 220 | nonlocal process |
| 221 | output_raw = bytearray() | ||
| 210 | starttime = time.time() | 222 | starttime = time.time() |
| 223 | progress = time.time() | ||
| 211 | process = subprocess.Popen(command, **options) | 224 | process = subprocess.Popen(command, **options) |
| 225 | has_timeout = False | ||
| 226 | appendline = None | ||
| 212 | if timeout: | 227 | if timeout: |
| 213 | endtime = starttime + timeout | 228 | endtime = starttime + timeout |
| 214 | eof = False | 229 | eof = False |
| 215 | while time.time() < endtime and not eof: | 230 | os.set_blocking(process.stdout.fileno(), False) |
| 216 | logger.debug('time: %s, endtime: %s' % (time.time(), endtime)) | 231 | while not has_timeout and not eof: |
| 217 | try: | 232 | try: |
| 218 | if select.select([process.stdout], [], [], 5)[0] != []: | 233 | if select.select([process.stdout], [], [], 5)[0] != []: |
| 219 | reader = codecs.getreader('utf-8')(process.stdout, 'ignore') | 234 | # wait a bit for more data, tries to avoid reading single characters |
| 220 | data = reader.read(1024, 4096) | 235 | time.sleep(0.2) |
| 236 | data = process.stdout.read() | ||
| 221 | if not data: | 237 | if not data: |
| 222 | process.stdout.close() | ||
| 223 | eof = True | 238 | eof = True |
| 224 | else: | 239 | else: |
| 225 | output += data | 240 | output_raw.extend(data) |
| 226 | logger.debug('Partial data from SSH call: %s' % data) | 241 | # ignore errors to capture as much as possible |
| 242 | #logger.debug('Partial data from SSH call:\n%s' % data.decode('utf-8', errors='ignore')) | ||
| 227 | endtime = time.time() + timeout | 243 | endtime = time.time() + timeout |
| 228 | except InterruptedError: | 244 | except InterruptedError: |
| 245 | logger.debug('InterruptedError') | ||
| 246 | continue | ||
| 247 | except BlockingIOError: | ||
| 248 | logger.debug('BlockingIOError') | ||
| 229 | continue | 249 | continue |
| 230 | 250 | ||
| 251 | if time.time() >= endtime: | ||
| 252 | logger.debug('SSHCall has timeout! Time: %s, endtime: %s' % (time.time(), endtime)) | ||
| 253 | has_timeout = True | ||
| 254 | |||
| 255 | if time.time() >= (progress + 60): | ||
| 256 | logger.debug('Waiting for process output at time: %s with datasize: %s' % (time.time(), len(output_raw))) | ||
| 257 | progress = time.time() | ||
| 258 | |||
| 259 | process.stdout.close() | ||
| 260 | |||
| 231 | # process hasn't returned yet | 261 | # process hasn't returned yet |
| 232 | if not eof: | 262 | if not eof: |
| 233 | process.terminate() | 263 | process.terminate() |
| @@ -235,20 +265,58 @@ def SSHCall(command, logger, timeout=None, **opts): | |||
| 235 | try: | 265 | try: |
| 236 | process.kill() | 266 | process.kill() |
| 237 | except OSError: | 267 | except OSError: |
| 268 | logger.debug('OSError when killing process') | ||
| 238 | pass | 269 | pass |
| 239 | endtime = time.time() - starttime | 270 | endtime = time.time() - starttime |
| 240 | lastline = ("\nProcess killed - no output for %d seconds. Total" | 271 | appendline = ("\nProcess killed - no output for %d seconds. Total" |
| 241 | " running time: %d seconds." % (timeout, endtime)) | 272 | " running time: %d seconds." % (timeout, endtime)) |
| 242 | logger.debug('Received data from SSH call %s ' % lastline) | 273 | logger.debug('Received data from SSH call:\n%s ' % appendline) |
| 243 | output += lastline | 274 | process.wait() |
| 244 | 275 | ||
| 276 | if raw: | ||
| 277 | output = bytes(output_raw) | ||
| 278 | if appendline: | ||
| 279 | output += bytes(appendline, "utf-8") | ||
| 280 | else: | ||
| 281 | output = output_raw.decode('utf-8', errors='ignore') | ||
| 282 | if appendline: | ||
| 283 | output += appendline | ||
| 245 | else: | 284 | else: |
| 246 | output = process.communicate()[0].decode('utf-8', errors='ignore') | 285 | output = output_raw = process.communicate()[0] |
| 247 | logger.debug('Data from SSH call: %s' % output.rstrip()) | 286 | if not raw: |
| 287 | output = output_raw.decode('utf-8', errors='ignore') | ||
| 288 | |||
| 289 | if len(output) < (64 * 1024): | ||
| 290 | if output.rstrip(): | ||
| 291 | logger.debug('Data from SSH call:\n%s' % output.rstrip()) | ||
| 292 | else: | ||
| 293 | logger.debug('No output from SSH call') | ||
| 294 | |||
| 295 | # timout or not, make sure process exits and is not hanging | ||
| 296 | if process.returncode == None: | ||
| 297 | try: | ||
| 298 | process.wait(timeout=5) | ||
| 299 | except TimeoutExpired: | ||
| 300 | try: | ||
| 301 | process.kill() | ||
| 302 | except OSError: | ||
| 303 | logger.debug('OSError') | ||
| 304 | pass | ||
| 305 | process.wait() | ||
| 306 | |||
| 307 | if has_timeout: | ||
| 308 | # Version of openssh before 8.6_p1 returns error code 0 when killed | ||
| 309 | # by a signal, when the timeout occurs we will receive a 0 error | ||
| 310 | # code because the process is been terminated and it's wrong because | ||
| 311 | # that value means success, but the process timed out. | ||
| 312 | # Afterwards, from version 8.6_p1 onwards, the returned code is 255. | ||
| 313 | # Fix this behaviour by checking the return code | ||
| 314 | if process.returncode == 0: | ||
| 315 | process.returncode = 255 | ||
| 248 | 316 | ||
| 249 | options = { | 317 | options = { |
| 250 | "stdout": subprocess.PIPE, | 318 | "stdout": subprocess.PIPE, |
| 251 | "stderr": subprocess.STDOUT, | 319 | "stderr": subprocess.STDOUT if not raw else None, |
| 252 | "stdin": None, | 320 | "stdin": None, |
| 253 | "shell": False, | 321 | "shell": False, |
| 254 | "bufsize": -1, | 322 | "bufsize": -1, |
| @@ -271,6 +339,9 @@ def SSHCall(command, logger, timeout=None, **opts): | |||
| 271 | # whilst running and ensure we don't leave a process behind. | 339 | # whilst running and ensure we don't leave a process behind. |
| 272 | if process.poll() is None: | 340 | if process.poll() is None: |
| 273 | process.kill() | 341 | process.kill() |
| 342 | if process.returncode == None: | ||
| 343 | process.wait() | ||
| 274 | logger.debug('Something went wrong, killing SSH process') | 344 | logger.debug('Something went wrong, killing SSH process') |
| 275 | raise | 345 | raise |
| 276 | return (process.wait(), output.rstrip()) | 346 | |
| 347 | return (process.returncode, output if raw else output.rstrip()) | ||
diff --git a/meta/lib/oeqa/core/tests/cases/timeout.py b/meta/lib/oeqa/core/tests/cases/timeout.py index 5dfecc7b7c..69cf969a67 100644 --- a/meta/lib/oeqa/core/tests/cases/timeout.py +++ b/meta/lib/oeqa/core/tests/cases/timeout.py | |||
| @@ -8,6 +8,7 @@ from time import sleep | |||
| 8 | 8 | ||
| 9 | from oeqa.core.case import OETestCase | 9 | from oeqa.core.case import OETestCase |
| 10 | from oeqa.core.decorator.oetimeout import OETimeout | 10 | from oeqa.core.decorator.oetimeout import OETimeout |
| 11 | from oeqa.core.decorator.depends import OETestDepends | ||
| 11 | 12 | ||
| 12 | class TimeoutTest(OETestCase): | 13 | class TimeoutTest(OETestCase): |
| 13 | 14 | ||
| @@ -19,3 +20,15 @@ class TimeoutTest(OETestCase): | |||
| 19 | def testTimeoutFail(self): | 20 | def testTimeoutFail(self): |
| 20 | sleep(2) | 21 | sleep(2) |
| 21 | self.assertTrue(True, msg='How is this possible?') | 22 | self.assertTrue(True, msg='How is this possible?') |
| 23 | |||
| 24 | |||
| 25 | def testTimeoutSkip(self): | ||
| 26 | self.skipTest("This test needs to be skipped, so that testTimeoutDepends()'s OETestDepends kicks in") | ||
| 27 | |||
| 28 | @OETestDepends(["timeout.TimeoutTest.testTimeoutSkip"]) | ||
| 29 | @OETimeout(3) | ||
| 30 | def testTimeoutDepends(self): | ||
| 31 | self.assertTrue(False, msg='How is this possible?') | ||
| 32 | |||
| 33 | def testTimeoutUnrelated(self): | ||
| 34 | sleep(6) | ||
diff --git a/meta/lib/oeqa/core/tests/common.py b/meta/lib/oeqa/core/tests/common.py index 88cc758ad3..bcc4fde632 100644 --- a/meta/lib/oeqa/core/tests/common.py +++ b/meta/lib/oeqa/core/tests/common.py | |||
| @@ -9,7 +9,6 @@ import os | |||
| 9 | 9 | ||
| 10 | import unittest | 10 | import unittest |
| 11 | import logging | 11 | import logging |
| 12 | import os | ||
| 13 | 12 | ||
| 14 | logger = logging.getLogger("oeqa") | 13 | logger = logging.getLogger("oeqa") |
| 15 | logger.setLevel(logging.INFO) | 14 | logger.setLevel(logging.INFO) |
diff --git a/meta/lib/oeqa/core/tests/test_data.py b/meta/lib/oeqa/core/tests/test_data.py index ac74098b78..acd726f3a0 100755 --- a/meta/lib/oeqa/core/tests/test_data.py +++ b/meta/lib/oeqa/core/tests/test_data.py | |||
| @@ -33,7 +33,7 @@ class TestData(TestBase): | |||
| 33 | 33 | ||
| 34 | def test_data_fail_wrong_variable(self): | 34 | def test_data_fail_wrong_variable(self): |
| 35 | expectedError = 'AssertionError' | 35 | expectedError = 'AssertionError' |
| 36 | d = {'IMAGE' : 'core-image-sato', 'ARCH' : 'arm'} | 36 | d = {'IMAGE' : 'core-image-weston', 'ARCH' : 'arm'} |
| 37 | 37 | ||
| 38 | tc = self._testLoader(d=d, modules=self.modules) | 38 | tc = self._testLoader(d=d, modules=self.modules) |
| 39 | results = tc.runTests() | 39 | results = tc.runTests() |
diff --git a/meta/lib/oeqa/core/tests/test_decorators.py b/meta/lib/oeqa/core/tests/test_decorators.py index b798bf7d33..5095f39948 100755 --- a/meta/lib/oeqa/core/tests/test_decorators.py +++ b/meta/lib/oeqa/core/tests/test_decorators.py | |||
| @@ -133,5 +133,11 @@ class TestTimeoutDecorator(TestBase): | |||
| 133 | msg = "OETestTimeout didn't restore SIGALRM" | 133 | msg = "OETestTimeout didn't restore SIGALRM" |
| 134 | self.assertIs(alarm_signal, signal.getsignal(signal.SIGALRM), msg=msg) | 134 | self.assertIs(alarm_signal, signal.getsignal(signal.SIGALRM), msg=msg) |
| 135 | 135 | ||
| 136 | def test_timeout_cancel(self): | ||
| 137 | tests = ['timeout.TimeoutTest.testTimeoutSkip', 'timeout.TimeoutTest.testTimeoutDepends', 'timeout.TimeoutTest.testTimeoutUnrelated'] | ||
| 138 | msg = 'Unrelated test failed to complete' | ||
| 139 | tc = self._testLoader(modules=self.modules, tests=tests) | ||
| 140 | self.assertTrue(tc.runTests().wasSuccessful(), msg=msg) | ||
| 141 | |||
| 136 | if __name__ == '__main__': | 142 | if __name__ == '__main__': |
| 137 | unittest.main() | 143 | unittest.main() |
diff --git a/meta/lib/oeqa/core/utils/concurrencytest.py b/meta/lib/oeqa/core/utils/concurrencytest.py index b2eb68fb02..d10f8f7f04 100644 --- a/meta/lib/oeqa/core/utils/concurrencytest.py +++ b/meta/lib/oeqa/core/utils/concurrencytest.py | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | # | 2 | # |
| 3 | # Copyright OpenEmbedded Contributors | ||
| 4 | # | ||
| 3 | # SPDX-License-Identifier: GPL-2.0-or-later | 5 | # SPDX-License-Identifier: GPL-2.0-or-later |
| 4 | # | 6 | # |
| 5 | # Modified for use in OE by Richard Purdie, 2018 | 7 | # Modified for use in OE by Richard Purdie, 2018 |
| @@ -48,11 +50,16 @@ _all__ = [ | |||
| 48 | # | 50 | # |
| 49 | class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): | 51 | class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): |
| 50 | 52 | ||
| 51 | def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests): | 53 | def __init__(self, target, semaphore, threadnum, totalinprocess, totaltests, output, finalresult): |
| 52 | super(BBThreadsafeForwardingResult, self).__init__(target, semaphore) | 54 | super(BBThreadsafeForwardingResult, self).__init__(target, semaphore) |
| 53 | self.threadnum = threadnum | 55 | self.threadnum = threadnum |
| 54 | self.totalinprocess = totalinprocess | 56 | self.totalinprocess = totalinprocess |
| 55 | self.totaltests = totaltests | 57 | self.totaltests = totaltests |
| 58 | self.buffer = True | ||
| 59 | self.outputbuf = output | ||
| 60 | self.finalresult = finalresult | ||
| 61 | self.finalresult.buffer = True | ||
| 62 | self.target = target | ||
| 56 | 63 | ||
| 57 | def _add_result_with_semaphore(self, method, test, *args, **kwargs): | 64 | def _add_result_with_semaphore(self, method, test, *args, **kwargs): |
| 58 | self.semaphore.acquire() | 65 | self.semaphore.acquire() |
| @@ -61,16 +68,19 @@ class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): | |||
| 61 | self.result.starttime[test.id()] = self._test_start.timestamp() | 68 | self.result.starttime[test.id()] = self._test_start.timestamp() |
| 62 | self.result.threadprogress[self.threadnum].append(test.id()) | 69 | self.result.threadprogress[self.threadnum].append(test.id()) |
| 63 | totalprogress = sum(len(x) for x in self.result.threadprogress.values()) | 70 | totalprogress = sum(len(x) for x in self.result.threadprogress.values()) |
| 64 | self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s)" % ( | 71 | self.result.progressinfo[test.id()] = "%s: %s/%s %s/%s (%ss) (%s failed) (%s)" % ( |
| 65 | self.threadnum, | 72 | self.threadnum, |
| 66 | len(self.result.threadprogress[self.threadnum]), | 73 | len(self.result.threadprogress[self.threadnum]), |
| 67 | self.totalinprocess, | 74 | self.totalinprocess, |
| 68 | totalprogress, | 75 | totalprogress, |
| 69 | self.totaltests, | 76 | self.totaltests, |
| 70 | "{0:.2f}".format(time.time()-self._test_start.timestamp()), | 77 | "{0:.2f}".format(time.time()-self._test_start.timestamp()), |
| 78 | self.target.failed_tests, | ||
| 71 | test.id()) | 79 | test.id()) |
| 72 | finally: | 80 | finally: |
| 73 | self.semaphore.release() | 81 | self.semaphore.release() |
| 82 | self.finalresult._stderr_buffer = io.StringIO(initial_value=self.outputbuf.getvalue().decode("utf-8")) | ||
| 83 | self.finalresult._stdout_buffer = io.StringIO() | ||
| 74 | super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs) | 84 | super(BBThreadsafeForwardingResult, self)._add_result_with_semaphore(method, test, *args, **kwargs) |
| 75 | 85 | ||
| 76 | class ProxyTestResult: | 86 | class ProxyTestResult: |
| @@ -183,35 +193,28 @@ class dummybuf(object): | |||
| 183 | # | 193 | # |
| 184 | class ConcurrentTestSuite(unittest.TestSuite): | 194 | class ConcurrentTestSuite(unittest.TestSuite): |
| 185 | 195 | ||
| 186 | def __init__(self, suite, processes, setupfunc, removefunc): | 196 | def __init__(self, suite, processes, setupfunc, removefunc, bb_vars): |
| 187 | super(ConcurrentTestSuite, self).__init__([suite]) | 197 | super(ConcurrentTestSuite, self).__init__([suite]) |
| 188 | self.processes = processes | 198 | self.processes = processes |
| 189 | self.setupfunc = setupfunc | 199 | self.setupfunc = setupfunc |
| 190 | self.removefunc = removefunc | 200 | self.removefunc = removefunc |
| 201 | self.bb_vars = bb_vars | ||
| 191 | 202 | ||
| 192 | def run(self, result): | 203 | def run(self, result): |
| 193 | tests, totaltests = fork_for_tests(self.processes, self) | 204 | testservers, totaltests = fork_for_tests(self.processes, self) |
| 194 | try: | 205 | try: |
| 195 | threads = {} | 206 | threads = {} |
| 196 | queue = Queue() | 207 | queue = Queue() |
| 197 | semaphore = threading.Semaphore(1) | 208 | semaphore = threading.Semaphore(1) |
| 198 | result.threadprogress = {} | 209 | result.threadprogress = {} |
| 199 | for i, (test, testnum) in enumerate(tests): | 210 | for i, (testserver, testnum, output) in enumerate(testservers): |
| 200 | result.threadprogress[i] = [] | 211 | result.threadprogress[i] = [] |
| 201 | process_result = BBThreadsafeForwardingResult( | 212 | process_result = BBThreadsafeForwardingResult( |
| 202 | ExtraResultsDecoderTestResult(result), | 213 | ExtraResultsDecoderTestResult(result), |
| 203 | semaphore, i, testnum, totaltests) | 214 | semaphore, i, testnum, totaltests, output, result) |
| 204 | # Force buffering of stdout/stderr so the console doesn't get corrupted by test output | ||
| 205 | # as per default in parent code | ||
| 206 | process_result.buffer = True | ||
| 207 | # We have to add a buffer object to stdout to keep subunit happy | ||
| 208 | process_result._stderr_buffer = io.StringIO() | ||
| 209 | process_result._stderr_buffer.buffer = dummybuf(process_result._stderr_buffer) | ||
| 210 | process_result._stdout_buffer = io.StringIO() | ||
| 211 | process_result._stdout_buffer.buffer = dummybuf(process_result._stdout_buffer) | ||
| 212 | reader_thread = threading.Thread( | 215 | reader_thread = threading.Thread( |
| 213 | target=self._run_test, args=(test, process_result, queue)) | 216 | target=self._run_test, args=(testserver, process_result, queue)) |
| 214 | threads[test] = reader_thread, process_result | 217 | threads[testserver] = reader_thread, process_result |
| 215 | reader_thread.start() | 218 | reader_thread.start() |
| 216 | while threads: | 219 | while threads: |
| 217 | finished_test = queue.get() | 220 | finished_test = queue.get() |
| @@ -222,13 +225,13 @@ class ConcurrentTestSuite(unittest.TestSuite): | |||
| 222 | process_result.stop() | 225 | process_result.stop() |
| 223 | raise | 226 | raise |
| 224 | finally: | 227 | finally: |
| 225 | for test in tests: | 228 | for testserver in testservers: |
| 226 | test[0]._stream.close() | 229 | testserver[0]._stream.close() |
| 227 | 230 | ||
| 228 | def _run_test(self, test, process_result, queue): | 231 | def _run_test(self, testserver, process_result, queue): |
| 229 | try: | 232 | try: |
| 230 | try: | 233 | try: |
| 231 | test.run(process_result) | 234 | testserver.run(process_result) |
| 232 | except Exception: | 235 | except Exception: |
| 233 | # The run logic itself failed | 236 | # The run logic itself failed |
| 234 | case = testtools.ErrorHolder( | 237 | case = testtools.ErrorHolder( |
| @@ -236,12 +239,12 @@ class ConcurrentTestSuite(unittest.TestSuite): | |||
| 236 | error=sys.exc_info()) | 239 | error=sys.exc_info()) |
| 237 | case.run(process_result) | 240 | case.run(process_result) |
| 238 | finally: | 241 | finally: |
| 239 | queue.put(test) | 242 | queue.put(testserver) |
| 240 | 243 | ||
| 241 | def fork_for_tests(concurrency_num, suite): | 244 | def fork_for_tests(concurrency_num, suite): |
| 242 | result = [] | 245 | testservers = [] |
| 243 | if 'BUILDDIR' in os.environ: | 246 | if 'BUILDDIR' in os.environ: |
| 244 | selftestdir = get_test_layer() | 247 | selftestdir = get_test_layer(suite.bb_vars['BBLAYERS']) |
| 245 | 248 | ||
| 246 | test_blocks = partition_tests(suite, concurrency_num) | 249 | test_blocks = partition_tests(suite, concurrency_num) |
| 247 | # Clear the tests from the original suite so it doesn't keep them alive | 250 | # Clear the tests from the original suite so it doesn't keep them alive |
| @@ -261,7 +264,7 @@ def fork_for_tests(concurrency_num, suite): | |||
| 261 | ourpid = os.getpid() | 264 | ourpid = os.getpid() |
| 262 | try: | 265 | try: |
| 263 | newbuilddir = None | 266 | newbuilddir = None |
| 264 | stream = os.fdopen(c2pwrite, 'wb', 1) | 267 | stream = os.fdopen(c2pwrite, 'wb') |
| 265 | os.close(c2pread) | 268 | os.close(c2pread) |
| 266 | 269 | ||
| 267 | (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite) | 270 | (builddir, newbuilddir) = suite.setupfunc("-st-" + str(ourpid), selftestdir, process_suite) |
| @@ -273,10 +276,11 @@ def fork_for_tests(concurrency_num, suite): | |||
| 273 | newsi = os.open(os.devnull, os.O_RDWR) | 276 | newsi = os.open(os.devnull, os.O_RDWR) |
| 274 | os.dup2(newsi, sys.stdin.fileno()) | 277 | os.dup2(newsi, sys.stdin.fileno()) |
| 275 | 278 | ||
| 279 | # Send stdout/stderr over the stream | ||
| 280 | os.dup2(c2pwrite, sys.stdout.fileno()) | ||
| 281 | os.dup2(c2pwrite, sys.stderr.fileno()) | ||
| 282 | |||
| 276 | subunit_client = TestProtocolClient(stream) | 283 | subunit_client = TestProtocolClient(stream) |
| 277 | # Force buffering of stdout/stderr so the console doesn't get corrupted by test output | ||
| 278 | # as per default in parent code | ||
| 279 | subunit_client.buffer = True | ||
| 280 | subunit_result = AutoTimingTestResultDecorator(subunit_client) | 284 | subunit_result = AutoTimingTestResultDecorator(subunit_client) |
| 281 | unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result)) | 285 | unittest_result = process_suite.run(ExtraResultsEncoderTestResult(subunit_result)) |
| 282 | if ourpid != os.getpid(): | 286 | if ourpid != os.getpid(): |
| @@ -305,10 +309,12 @@ def fork_for_tests(concurrency_num, suite): | |||
| 305 | os._exit(0) | 309 | os._exit(0) |
| 306 | else: | 310 | else: |
| 307 | os.close(c2pwrite) | 311 | os.close(c2pwrite) |
| 308 | stream = os.fdopen(c2pread, 'rb', 1) | 312 | stream = os.fdopen(c2pread, 'rb') |
| 309 | test = ProtocolTestCase(stream) | 313 | # Collect stdout/stderr into an io buffer |
| 310 | result.append((test, numtests)) | 314 | output = io.BytesIO() |
| 311 | return result, totaltests | 315 | testserver = ProtocolTestCase(stream, passthrough=output) |
| 316 | testservers.append((testserver, numtests, output)) | ||
| 317 | return testservers, totaltests | ||
| 312 | 318 | ||
| 313 | def partition_tests(suite, count): | 319 | def partition_tests(suite, count): |
| 314 | # Keep tests from the same class together but allow tests from modules | 320 | # 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 deleted file mode 100644 index e1a59588eb..0000000000 --- a/meta/lib/oeqa/core/utils/misc.py +++ /dev/null | |||
| @@ -1,47 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright (C) 2016 Intel Corporation | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: MIT | ||
| 5 | # | ||
| 6 | |||
| 7 | def toList(obj, obj_type, obj_name="Object"): | ||
| 8 | if isinstance(obj, obj_type): | ||
| 9 | return [obj] | ||
| 10 | elif isinstance(obj, list): | ||
| 11 | return obj | ||
| 12 | else: | ||
| 13 | raise TypeError("%s must be %s or list" % (obj_name, obj_type)) | ||
| 14 | |||
| 15 | def toSet(obj, obj_type, obj_name="Object"): | ||
| 16 | if isinstance(obj, obj_type): | ||
| 17 | return {obj} | ||
| 18 | elif isinstance(obj, list): | ||
| 19 | return set(obj) | ||
| 20 | elif isinstance(obj, set): | ||
| 21 | return obj | ||
| 22 | else: | ||
| 23 | raise TypeError("%s must be %s or set" % (obj_name, obj_type)) | ||
| 24 | |||
| 25 | def strToList(obj, obj_name="Object"): | ||
| 26 | return toList(obj, str, obj_name) | ||
| 27 | |||
| 28 | def strToSet(obj, obj_name="Object"): | ||
| 29 | return toSet(obj, str, obj_name) | ||
| 30 | |||
| 31 | def intToList(obj, obj_name="Object"): | ||
| 32 | return toList(obj, int, obj_name) | ||
| 33 | |||
| 34 | def dataStoteToDict(d, variables): | ||
| 35 | data = {} | ||
| 36 | |||
| 37 | for v in variables: | ||
| 38 | data[v] = d.getVar(v) | ||
| 39 | |||
| 40 | return data | ||
| 41 | |||
| 42 | def updateTestData(d, td, variables): | ||
| 43 | """ | ||
| 44 | Updates variables with values of data store to test data. | ||
| 45 | """ | ||
| 46 | for var in variables: | ||
| 47 | td[var] = d.getVar(var) | ||
