summaryrefslogtreecommitdiffstats
path: root/meta/lib/oeqa/core
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oeqa/core')
-rw-r--r--meta/lib/oeqa/core/case.py9
-rw-r--r--meta/lib/oeqa/core/context.py2
-rw-r--r--meta/lib/oeqa/core/decorator/__init__.py11
-rw-r--r--meta/lib/oeqa/core/decorator/data.py76
-rw-r--r--meta/lib/oeqa/core/decorator/oetimeout.py5
-rw-r--r--meta/lib/oeqa/core/loader.py12
-rw-r--r--meta/lib/oeqa/core/runner.py12
-rw-r--r--meta/lib/oeqa/core/target/qemu.py40
-rw-r--r--meta/lib/oeqa/core/target/ssh.py81
-rw-r--r--meta/lib/oeqa/core/tests/cases/timeout.py13
-rwxr-xr-xmeta/lib/oeqa/core/tests/test_data.py2
-rwxr-xr-xmeta/lib/oeqa/core/tests/test_decorators.py6
-rw-r--r--meta/lib/oeqa/core/utils/concurrencytest.py68
-rw-r--r--meta/lib/oeqa/core/utils/misc.py47
14 files changed, 228 insertions, 156 deletions
diff --git a/meta/lib/oeqa/core/case.py b/meta/lib/oeqa/core/case.py
index aae451fef2..bc4446a938 100644
--- a/meta/lib/oeqa/core/case.py
+++ b/meta/lib/oeqa/core/case.py
@@ -43,8 +43,13 @@ class OETestCase(unittest.TestCase):
43 clss.tearDownClassMethod() 43 clss.tearDownClassMethod()
44 44
45 def _oeSetUp(self): 45 def _oeSetUp(self):
46 for d in self.decorators: 46 try:
47 d.setUpDecorator() 47 for d in self.decorators:
48 d.setUpDecorator()
49 except:
50 for d in self.decorators:
51 d.tearDownDecorator()
52 raise
48 self.setUpMethod() 53 self.setUpMethod()
49 54
50 def _oeTearDown(self): 55 def _oeTearDown(self):
diff --git a/meta/lib/oeqa/core/context.py b/meta/lib/oeqa/core/context.py
index 2abe353d27..9313271f58 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()
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
7from functools import wraps 7from functools import wraps
8from abc import abstractmethod, ABCMeta 8from abc import ABCMeta
9from oeqa.core.utils.misc import strToList
10 9
11decoratorClasses = set() 10decoratorClasses = set()
12 11
@@ -65,15 +64,11 @@ class OETestDiscover(OETestDecorator):
65 return registry['cases'] 64 return registry['cases']
66 65
67def OETestTag(*tags): 66def 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..5444b2cb75 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
30def 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,53 @@ class skipIfMachine(OETestDecorator):
189@registerDecorator 178@registerDecorator
190class skipIfNotQemu(OETestDecorator): 179class 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 189class 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
207class skipIfQemu(OETestDecorator): 199class skipIfQemu(OETestDecorator):
208 """ 200 """
209 Skip test based on Qemu Machine. 201 Skip test if MACHINE is qemu*
210 202 """
211 value must not be a qemu machine or it will skip the test 203 def setUpDecorator(self):
212 with msg as the reason. 204 self.logger.debug("Checking if qemu MACHINE")
213 """ 205 if self.case.td.get('MACHINE', '').startswith('qemu'):
206 self.case.skipTest('Test only runs on real hardware')
214 207
215 attrs = ('value', 'msg') 208@registerDecorator
209class skipIfArch(OETestDecorator):
210 """
211 Skip test if HOST_ARCH is present in the tuple specified.
212 """
216 213
214 attrs = ('archs',)
217 def setUpDecorator(self): 215 def setUpDecorator(self):
218 msg = ('Checking if %s is this MACHINE' % self.value) 216 arch = self.case.td['HOST_ARCH']
219 self.logger.debug(msg) 217 if arch in self.archs:
220 if is_qemu(self.case.td, self.value): 218 self.case.skipTest('Test skipped on %s' % arch)
221 self.case.skipTest(self.msg) 219
220@registerDecorator
221class skipIfNotArch(OETestDecorator):
222 """
223 Skip test if HOST_ARCH is not present in the tuple specified.
224 """
222 225
226 attrs = ('archs',)
227 def setUpDecorator(self):
228 arch = self.case.td['HOST_ARCH']
229 if arch not in self.archs:
230 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
40def _built_modules_dict(modules): 40def _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..a86a706bd9 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
232class OEListTestsResult(object): 242class OEListTestsResult(object):
233 def wasSuccessful(self): 243 def wasSuccessful(self):
234 return True 244 return True
diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py
index 0f29414df5..d93b3ac94a 100644
--- a/meta/lib/oeqa/core/target/qemu.py
+++ b/meta/lib/oeqa/core/target/qemu.py
@@ -8,20 +8,21 @@ import os
8import sys 8import sys
9import signal 9import signal
10import time 10import time
11import glob
12import subprocess
11from collections import defaultdict 13from collections import defaultdict
12 14
13from .ssh import OESSHTarget 15from .ssh import OESSHTarget
14from oeqa.utils.qemurunner import QemuRunner 16from oeqa.utils.qemurunner import QemuRunner
15from oeqa.utils.dump import TargetDumper
16 17
17supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] 18supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic']
18 19
19class OEQemuTarget(OESSHTarget): 20class 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/ssh.py b/meta/lib/oeqa/core/target/ssh.py
index 461448dbc5..09cdd14c75 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 ]
@@ -67,7 +72,7 @@ class OESSHTarget(OETarget):
67 72
68 return (status, output) 73 return (status, output)
69 74
70 def run(self, command, timeout=None): 75 def run(self, command, timeout=None, ignore_status=True):
71 """ 76 """
72 Runs command in target. 77 Runs command in target.
73 78
@@ -86,10 +91,9 @@ class OESSHTarget(OETarget):
86 else: 91 else:
87 processTimeout = self.timeout 92 processTimeout = self.timeout
88 93
89 status, output = self._run(sshCmd, processTimeout, True) 94 status, output = self._run(sshCmd, processTimeout, ignore_status)
90 self.logger.debug('Command: %s\nOutput: %s\n' % (command, output)) 95 self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output))
91 if (status == 255) and (('No route to host') in output): 96
92 self.target_dumper.dump_target()
93 return (status, output) 97 return (status, output)
94 98
95 def copyTo(self, localSrc, remoteDst): 99 def copyTo(self, localSrc, remoteDst):
@@ -207,27 +211,41 @@ def SSHCall(command, logger, timeout=None, **opts):
207 def run(): 211 def run():
208 nonlocal output 212 nonlocal output
209 nonlocal process 213 nonlocal process
214 output_raw = b''
210 starttime = time.time() 215 starttime = time.time()
211 process = subprocess.Popen(command, **options) 216 process = subprocess.Popen(command, **options)
217 has_timeout = False
212 if timeout: 218 if timeout:
213 endtime = starttime + timeout 219 endtime = starttime + timeout
214 eof = False 220 eof = False
215 while time.time() < endtime and not eof: 221 os.set_blocking(process.stdout.fileno(), False)
216 logger.debug('time: %s, endtime: %s' % (time.time(), endtime)) 222 while not has_timeout and not eof:
217 try: 223 try:
224 logger.debug('Waiting for process output: time: %s, endtime: %s' % (time.time(), endtime))
218 if select.select([process.stdout], [], [], 5)[0] != []: 225 if select.select([process.stdout], [], [], 5)[0] != []:
219 reader = codecs.getreader('utf-8')(process.stdout, 'ignore') 226 # wait a bit for more data, tries to avoid reading single characters
220 data = reader.read(1024, 4096) 227 time.sleep(0.2)
228 data = process.stdout.read()
221 if not data: 229 if not data:
222 process.stdout.close()
223 eof = True 230 eof = True
224 else: 231 else:
225 output += data 232 output_raw += data
226 logger.debug('Partial data from SSH call: %s' % data) 233 # ignore errors to capture as much as possible
234 logger.debug('Partial data from SSH call:\n%s' % data.decode('utf-8', errors='ignore'))
227 endtime = time.time() + timeout 235 endtime = time.time() + timeout
228 except InterruptedError: 236 except InterruptedError:
237 logger.debug('InterruptedError')
238 continue
239 except BlockingIOError:
240 logger.debug('BlockingIOError')
229 continue 241 continue
230 242
243 if time.time() >= endtime:
244 logger.debug('SSHCall has timeout! Time: %s, endtime: %s' % (time.time(), endtime))
245 has_timeout = True
246
247 process.stdout.close()
248
231 # process hasn't returned yet 249 # process hasn't returned yet
232 if not eof: 250 if not eof:
233 process.terminate() 251 process.terminate()
@@ -235,16 +253,42 @@ def SSHCall(command, logger, timeout=None, **opts):
235 try: 253 try:
236 process.kill() 254 process.kill()
237 except OSError: 255 except OSError:
256 logger.debug('OSError when killing process')
238 pass 257 pass
239 endtime = time.time() - starttime 258 endtime = time.time() - starttime
240 lastline = ("\nProcess killed - no output for %d seconds. Total" 259 lastline = ("\nProcess killed - no output for %d seconds. Total"
241 " running time: %d seconds." % (timeout, endtime)) 260 " running time: %d seconds." % (timeout, endtime))
242 logger.debug('Received data from SSH call %s ' % lastline) 261 logger.debug('Received data from SSH call:\n%s ' % lastline)
243 output += lastline 262 output += lastline
263 process.wait()
244 264
245 else: 265 else:
246 output = process.communicate()[0].decode('utf-8', errors='ignore') 266 output_raw = process.communicate()[0]
247 logger.debug('Data from SSH call: %s' % output.rstrip()) 267
268 output = output_raw.decode('utf-8', errors='ignore')
269 logger.debug('Data from SSH call:\n%s' % output.rstrip())
270
271 # timout or not, make sure process exits and is not hanging
272 if process.returncode == None:
273 try:
274 process.wait(timeout=5)
275 except TimeoutExpired:
276 try:
277 process.kill()
278 except OSError:
279 logger.debug('OSError')
280 pass
281 process.wait()
282
283 if has_timeout:
284 # Version of openssh before 8.6_p1 returns error code 0 when killed
285 # by a signal, when the timeout occurs we will receive a 0 error
286 # code because the process is been terminated and it's wrong because
287 # that value means success, but the process timed out.
288 # Afterwards, from version 8.6_p1 onwards, the returned code is 255.
289 # Fix this behaviour by checking the return code
290 if process.returncode == 0:
291 process.returncode = 255
248 292
249 options = { 293 options = {
250 "stdout": subprocess.PIPE, 294 "stdout": subprocess.PIPE,
@@ -271,6 +315,9 @@ def SSHCall(command, logger, timeout=None, **opts):
271 # whilst running and ensure we don't leave a process behind. 315 # whilst running and ensure we don't leave a process behind.
272 if process.poll() is None: 316 if process.poll() is None:
273 process.kill() 317 process.kill()
318 if process.returncode == None:
319 process.wait()
274 logger.debug('Something went wrong, killing SSH process') 320 logger.debug('Something went wrong, killing SSH process')
275 raise 321 raise
276 return (process.wait(), output.rstrip()) 322
323 return (process.returncode, 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
9from oeqa.core.case import OETestCase 9from oeqa.core.case import OETestCase
10from oeqa.core.decorator.oetimeout import OETimeout 10from oeqa.core.decorator.oetimeout import OETimeout
11from oeqa.core.decorator.depends import OETestDepends
11 12
12class TimeoutTest(OETestCase): 13class 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/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
136if __name__ == '__main__': 142if __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#
49class BBThreadsafeForwardingResult(ThreadsafeForwardingResult): 51class 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
76class ProxyTestResult: 86class ProxyTestResult:
@@ -183,35 +193,28 @@ class dummybuf(object):
183# 193#
184class ConcurrentTestSuite(unittest.TestSuite): 194class 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
241def fork_for_tests(concurrency_num, suite): 244def 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
313def partition_tests(suite, count): 319def 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
7def 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
15def 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
25def strToList(obj, obj_name="Object"):
26 return toList(obj, str, obj_name)
27
28def strToSet(obj, obj_name="Object"):
29 return toSet(obj, str, obj_name)
30
31def intToList(obj, obj_name="Object"):
32 return toList(obj, int, obj_name)
33
34def dataStoteToDict(d, variables):
35 data = {}
36
37 for v in variables:
38 data[v] = d.getVar(v)
39
40 return data
41
42def 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)