summaryrefslogtreecommitdiffstats
path: root/meta/lib/oeqa/utils
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oeqa/utils')
-rw-r--r--meta/lib/oeqa/utils/__init__.py15
-rw-r--r--meta/lib/oeqa/utils/buildproject.py3
-rw-r--r--meta/lib/oeqa/utils/commands.py81
-rw-r--r--meta/lib/oeqa/utils/decorators.py85
-rw-r--r--meta/lib/oeqa/utils/dump.py89
-rw-r--r--meta/lib/oeqa/utils/ftools.py2
-rw-r--r--meta/lib/oeqa/utils/gitarchive.py62
-rw-r--r--meta/lib/oeqa/utils/httpserver.py29
-rw-r--r--meta/lib/oeqa/utils/logparser.py98
-rw-r--r--meta/lib/oeqa/utils/metadata.py11
-rw-r--r--meta/lib/oeqa/utils/network.py2
-rw-r--r--meta/lib/oeqa/utils/nfs.py10
-rw-r--r--meta/lib/oeqa/utils/package_manager.py2
-rw-r--r--meta/lib/oeqa/utils/postactions.py102
-rw-r--r--meta/lib/oeqa/utils/qemurunner.py469
-rw-r--r--meta/lib/oeqa/utils/qemutinyrunner.py6
-rw-r--r--meta/lib/oeqa/utils/sshcontrol.py6
-rw-r--r--meta/lib/oeqa/utils/subprocesstweak.py15
-rw-r--r--meta/lib/oeqa/utils/targetbuild.py4
-rw-r--r--meta/lib/oeqa/utils/testexport.py10
20 files changed, 771 insertions, 330 deletions
diff --git a/meta/lib/oeqa/utils/__init__.py b/meta/lib/oeqa/utils/__init__.py
index 6d1ec4cb99..e03f7e33bb 100644
--- a/meta/lib/oeqa/utils/__init__.py
+++ b/meta/lib/oeqa/utils/__init__.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4# Enable other layers to have modules in the same named directory 6# Enable other layers to have modules in the same named directory
@@ -88,3 +90,16 @@ def load_test_components(logger, executor):
88 "_executor_class defined." % (comp_name, comp_context)) 90 "_executor_class defined." % (comp_name, comp_context))
89 91
90 return components 92 return components
93
94def get_json_result_dir(d):
95 json_result_dir = os.path.join(d.getVar("LOG_DIR"), 'oeqa')
96 custom_json_result_dir = d.getVar("OEQA_JSON_RESULT_DIR")
97 if custom_json_result_dir:
98 json_result_dir = custom_json_result_dir
99 return json_result_dir
100
101def get_artefact_dir(d):
102 custom_json_result_dir = d.getVar("OEQA_ARTEFACT_DIR")
103 if custom_json_result_dir:
104 return custom_json_result_dir
105 return os.path.join(d.getVar("LOG_DIR"), 'oeqa-artefacts')
diff --git a/meta/lib/oeqa/utils/buildproject.py b/meta/lib/oeqa/utils/buildproject.py
index e6d80cc8dc..dfb9661868 100644
--- a/meta/lib/oeqa/utils/buildproject.py
+++ b/meta/lib/oeqa/utils/buildproject.py
@@ -18,6 +18,7 @@ class BuildProject(metaclass=ABCMeta):
18 def __init__(self, uri, foldername=None, tmpdir=None, dl_dir=None): 18 def __init__(self, uri, foldername=None, tmpdir=None, dl_dir=None):
19 self.uri = uri 19 self.uri = uri
20 self.archive = os.path.basename(uri) 20 self.archive = os.path.basename(uri)
21 self.tempdirobj = None
21 if not tmpdir: 22 if not tmpdir:
22 self.tempdirobj = tempfile.TemporaryDirectory(prefix='buildproject-') 23 self.tempdirobj = tempfile.TemporaryDirectory(prefix='buildproject-')
23 tmpdir = self.tempdirobj.name 24 tmpdir = self.tempdirobj.name
@@ -57,6 +58,8 @@ class BuildProject(metaclass=ABCMeta):
57 return self._run('cd %s; make install %s' % (self.targetdir, install_args)) 58 return self._run('cd %s; make install %s' % (self.targetdir, install_args))
58 59
59 def clean(self): 60 def clean(self):
61 if self.tempdirobj:
62 self.tempdirobj.cleanup()
60 if not self.needclean: 63 if not self.needclean:
61 return 64 return
62 self._run('rm -rf %s' % self.targetdir) 65 self._run('rm -rf %s' % self.targetdir)
diff --git a/meta/lib/oeqa/utils/commands.py b/meta/lib/oeqa/utils/commands.py
index a71c16ab14..b60a6e6c38 100644
--- a/meta/lib/oeqa/utils/commands.py
+++ b/meta/lib/oeqa/utils/commands.py
@@ -8,11 +8,8 @@
8# This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest 8# This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest
9# It provides a class and methods for running commands on the host in a convienent way for tests. 9# It provides a class and methods for running commands on the host in a convienent way for tests.
10 10
11
12
13import os 11import os
14import sys 12import sys
15import signal
16import subprocess 13import subprocess
17import threading 14import threading
18import time 15import time
@@ -21,6 +18,7 @@ from oeqa.utils import CommandError
21from oeqa.utils import ftools 18from oeqa.utils import ftools
22import re 19import re
23import contextlib 20import contextlib
21import errno
24# Export test doesn't require bb 22# Export test doesn't require bb
25try: 23try:
26 import bb 24 import bb
@@ -85,7 +83,7 @@ class Command(object):
85 except OSError as ex: 83 except OSError as ex:
86 # It's not an error when the command does not consume all 84 # It's not an error when the command does not consume all
87 # of our data. subprocess.communicate() also ignores that. 85 # of our data. subprocess.communicate() also ignores that.
88 if ex.errno != EPIPE: 86 if ex.errno != errno.EPIPE:
89 raise 87 raise
90 88
91 # We write in a separate thread because then we can read 89 # We write in a separate thread because then we can read
@@ -117,7 +115,7 @@ class Command(object):
117 else: 115 else:
118 deadline = time.time() + self.timeout 116 deadline = time.time() + self.timeout
119 for thread in self.threads: 117 for thread in self.threads:
120 timeout = deadline - time.time() 118 timeout = deadline - time.time()
121 if timeout < 0: 119 if timeout < 0:
122 timeout = 0 120 timeout = 0
123 thread.join(timeout) 121 thread.join(timeout)
@@ -168,18 +166,22 @@ class Result(object):
168 166
169 167
170def runCmd(command, ignore_status=False, timeout=None, assert_error=True, sync=True, 168def runCmd(command, ignore_status=False, timeout=None, assert_error=True, sync=True,
171 native_sysroot=None, limit_exc_output=0, output_log=None, **options): 169 native_sysroot=None, target_sys=None, limit_exc_output=0, output_log=None, **options):
172 result = Result() 170 result = Result()
173 171
174 if native_sysroot: 172 if native_sysroot:
175 extra_paths = "%s/sbin:%s/usr/sbin:%s/usr/bin" % \ 173 new_env = dict(options.get('env', os.environ))
176 (native_sysroot, native_sysroot, native_sysroot) 174 paths = new_env["PATH"].split(":")
177 extra_libpaths = "%s/lib:%s/usr/lib" % \ 175 paths = [
178 (native_sysroot, native_sysroot) 176 os.path.join(native_sysroot, "bin"),
179 nenv = dict(options.get('env', os.environ)) 177 os.path.join(native_sysroot, "sbin"),
180 nenv['PATH'] = extra_paths + ':' + nenv.get('PATH', '') 178 os.path.join(native_sysroot, "usr", "bin"),
181 nenv['LD_LIBRARY_PATH'] = extra_libpaths + ':' + nenv.get('LD_LIBRARY_PATH', '') 179 os.path.join(native_sysroot, "usr", "sbin"),
182 options['env'] = nenv 180 ] + paths
181 if target_sys:
182 paths = [os.path.join(native_sysroot, "usr", "bin", target_sys)] + paths
183 new_env["PATH"] = ":".join(paths)
184 options['env'] = new_env
183 185
184 cmd = Command(command, timeout=timeout, output_log=output_log, **options) 186 cmd = Command(command, timeout=timeout, output_log=output_log, **options)
185 cmd.run() 187 cmd.run()
@@ -201,6 +203,8 @@ def runCmd(command, ignore_status=False, timeout=None, assert_error=True, sync=T
201 203
202 if result.status and not ignore_status: 204 if result.status and not ignore_status:
203 exc_output = result.output 205 exc_output = result.output
206 if result.error:
207 exc_output = exc_output + result.error
204 if limit_exc_output > 0: 208 if limit_exc_output > 0:
205 split = result.output.splitlines() 209 split = result.output.splitlines()
206 if len(split) > limit_exc_output: 210 if len(split) > limit_exc_output:
@@ -281,10 +285,25 @@ def get_bb_vars(variables=None, target=None, postconfig=None):
281 return values 285 return values
282 286
283def get_bb_var(var, target=None, postconfig=None): 287def get_bb_var(var, target=None, postconfig=None):
284 return get_bb_vars([var], target, postconfig)[var] 288 if postconfig:
285 289 return bitbake("-e %s" % target or "", postconfig=postconfig).output
286def get_test_layer(): 290 else:
287 layers = get_bb_var("BBLAYERS").split() 291 # Fast-path for the non-postconfig case
292 cmd = ["bitbake-getvar", "--quiet", "--value", var]
293 if target:
294 cmd.extend(["--recipe", target])
295 try:
296 return subprocess.run(cmd, check=True, text=True, stdout=subprocess.PIPE).stdout.strip()
297 except subprocess.CalledProcessError as e:
298 # We need to return None not the empty string if the variable hasn't been set.
299 if e.returncode == 1:
300 return None
301 raise
302
303def get_test_layer(bblayers=None):
304 if bblayers is None:
305 bblayers = get_bb_var("BBLAYERS")
306 layers = bblayers.split()
288 testlayer = None 307 testlayer = None
289 for l in layers: 308 for l in layers:
290 if '~' in l: 309 if '~' in l:
@@ -296,6 +315,7 @@ def get_test_layer():
296 315
297def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'): 316def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
298 os.makedirs(os.path.join(templayerdir, 'conf')) 317 os.makedirs(os.path.join(templayerdir, 'conf'))
318 corenames = get_bb_var('LAYERSERIES_CORENAMES')
299 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f: 319 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
300 f.write('BBPATH .= ":${LAYERDIR}"\n') 320 f.write('BBPATH .= ":${LAYERDIR}"\n')
301 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec) 321 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
@@ -304,12 +324,29 @@ def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec=
304 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername) 324 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
305 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority)) 325 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
306 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername) 326 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
307 f.write('LAYERSERIES_COMPAT_%s = "${LAYERSERIES_COMPAT_core}"\n' % templayername) 327 f.write('LAYERSERIES_COMPAT_%s = "%s"\n' % (templayername, corenames))
308 328
309@contextlib.contextmanager 329@contextlib.contextmanager
310def runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None, qemuparams=None, overrides={}, discard_writes=True): 330def runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None, qemuparams=None, overrides={}, boot_patterns = {}, discard_writes=True):
311 """ 331 """
312 launch_cmd means directly run the command, don't need set rootfs or env vars. 332 Starts a context manager for a 'oeqa.targetcontrol.QemuTarget' resource.
333 The underlying Qemu will be booted into a shell when the generator yields
334 and stopped when the 'with' block exits.
335
336 Usage:
337
338 with runqemu('core-image-minimal') as qemu:
339 qemu.run_serial('cat /proc/cpuinfo')
340
341 Args:
342 pn (str): (image) recipe to run on
343 ssh (boolean): whether or not to enable SSH (network access)
344 runqemuparams (str): space-separated list of params to pass to 'runqemu' script (like 'nographics', 'ovmf', etc.)
345 image_fstype (str): IMAGE_FSTYPE to use
346 launch_cmd (str): directly run this command and bypass automatic runqemu parameter generation
347 overrides (dict): dict of "'<bitbake-variable>': value" pairs that allows overriding bitbake variables
348 boot_patterns (dict): dict of "'<pattern-name>': value" pairs to override default boot patterns, e.g. when not booting Linux
349 discard_writes (boolean): enables qemu -snapshot feature to prevent modifying original image
313 """ 350 """
314 351
315 import bb.tinfoil 352 import bb.tinfoil
@@ -340,7 +377,7 @@ def runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None,
340 377
341 logdir = recipedata.getVar("TEST_LOG_DIR") 378 logdir = recipedata.getVar("TEST_LOG_DIR")
342 379
343 qemu = oeqa.targetcontrol.QemuTarget(recipedata, targetlogger, image_fstype) 380 qemu = oeqa.targetcontrol.QemuTarget(recipedata, targetlogger, image_fstype, boot_patterns=boot_patterns)
344 finally: 381 finally:
345 # We need to shut down tinfoil early here in case we actually want 382 # We need to shut down tinfoil early here in case we actually want
346 # to run tinfoil-using utilities with the running QEMU instance. 383 # to run tinfoil-using utilities with the running QEMU instance.
diff --git a/meta/lib/oeqa/utils/decorators.py b/meta/lib/oeqa/utils/decorators.py
index aabf4110cb..ea90164e5e 100644
--- a/meta/lib/oeqa/utils/decorators.py
+++ b/meta/lib/oeqa/utils/decorators.py
@@ -16,91 +16,6 @@ import threading
16import signal 16import signal
17from functools import wraps 17from functools import wraps
18 18
19#get the "result" object from one of the upper frames provided that one of these upper frames is a unittest.case frame
20class getResults(object):
21 def __init__(self):
22 #dynamically determine the unittest.case frame and use it to get the name of the test method
23 ident = threading.current_thread().ident
24 upperf = sys._current_frames()[ident]
25 while (upperf.f_globals['__name__'] != 'unittest.case'):
26 upperf = upperf.f_back
27
28 def handleList(items):
29 ret = []
30 # items is a list of tuples, (test, failure) or (_ErrorHandler(), Exception())
31 for i in items:
32 s = i[0].id()
33 #Handle the _ErrorHolder objects from skipModule failures
34 if "setUpModule (" in s:
35 ret.append(s.replace("setUpModule (", "").replace(")",""))
36 else:
37 ret.append(s)
38 # Append also the test without the full path
39 testname = s.split('.')[-1]
40 if testname:
41 ret.append(testname)
42 return ret
43 self.faillist = handleList(upperf.f_locals['result'].failures)
44 self.errorlist = handleList(upperf.f_locals['result'].errors)
45 self.skiplist = handleList(upperf.f_locals['result'].skipped)
46
47 def getFailList(self):
48 return self.faillist
49
50 def getErrorList(self):
51 return self.errorlist
52
53 def getSkipList(self):
54 return self.skiplist
55
56class skipIfFailure(object):
57
58 def __init__(self,testcase):
59 self.testcase = testcase
60
61 def __call__(self,f):
62 @wraps(f)
63 def wrapped_f(*args, **kwargs):
64 res = getResults()
65 if self.testcase in (res.getFailList() or res.getErrorList()):
66 raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
67 return f(*args, **kwargs)
68 wrapped_f.__name__ = f.__name__
69 return wrapped_f
70
71class skipIfSkipped(object):
72
73 def __init__(self,testcase):
74 self.testcase = testcase
75
76 def __call__(self,f):
77 @wraps(f)
78 def wrapped_f(*args, **kwargs):
79 res = getResults()
80 if self.testcase in res.getSkipList():
81 raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
82 return f(*args, **kwargs)
83 wrapped_f.__name__ = f.__name__
84 return wrapped_f
85
86class skipUnlessPassed(object):
87
88 def __init__(self,testcase):
89 self.testcase = testcase
90
91 def __call__(self,f):
92 @wraps(f)
93 def wrapped_f(*args, **kwargs):
94 res = getResults()
95 if self.testcase in res.getSkipList() or \
96 self.testcase in res.getFailList() or \
97 self.testcase in res.getErrorList():
98 raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
99 return f(*args, **kwargs)
100 wrapped_f.__name__ = f.__name__
101 wrapped_f._depends_on = self.testcase
102 return wrapped_f
103
104class testcase(object): 19class testcase(object):
105 def __init__(self, test_case): 20 def __init__(self, test_case):
106 self.test_case = test_case 21 self.test_case = test_case
diff --git a/meta/lib/oeqa/utils/dump.py b/meta/lib/oeqa/utils/dump.py
index 09a44329e0..d4d271369f 100644
--- a/meta/lib/oeqa/utils/dump.py
+++ b/meta/lib/oeqa/utils/dump.py
@@ -1,9 +1,12 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
5import os 7import os
6import sys 8import sys
9import json
7import errno 10import errno
8import datetime 11import datetime
9import itertools 12import itertools
@@ -17,6 +20,7 @@ class BaseDumper(object):
17 # Some testing doesn't inherit testimage, so it is needed 20 # Some testing doesn't inherit testimage, so it is needed
18 # to set some defaults. 21 # to set some defaults.
19 self.parent_dir = parent_dir 22 self.parent_dir = parent_dir
23 self.dump_dir = parent_dir
20 dft_cmds = """ top -bn1 24 dft_cmds = """ top -bn1
21 iostat -x -z -N -d -p ALL 20 2 25 iostat -x -z -N -d -p ALL 20 2
22 ps -ef 26 ps -ef
@@ -46,11 +50,11 @@ class BaseDumper(object):
46 raise err 50 raise err
47 self.dump_dir = dump_dir 51 self.dump_dir = dump_dir
48 52
49 def _write_dump(self, command, output): 53 def _construct_filename(self, command):
50 if isinstance(self, HostDumper): 54 if isinstance(self, TargetDumper):
51 prefix = "host"
52 elif isinstance(self, TargetDumper):
53 prefix = "target" 55 prefix = "target"
56 elif isinstance(self, MonitorDumper):
57 prefix = "qmp"
54 else: 58 else:
55 prefix = "unknown" 59 prefix = "unknown"
56 for i in itertools.count(): 60 for i in itertools.count():
@@ -58,41 +62,80 @@ class BaseDumper(object):
58 fullname = os.path.join(self.dump_dir, filename) 62 fullname = os.path.join(self.dump_dir, filename)
59 if not os.path.exists(fullname): 63 if not os.path.exists(fullname):
60 break 64 break
61 with open(fullname, 'w') as dump_file: 65 return fullname
62 dump_file.write(output)
63
64
65class HostDumper(BaseDumper):
66 """ Class to get dumps from the host running the tests """
67
68 def __init__(self, cmds, parent_dir):
69 super(HostDumper, self).__init__(cmds, parent_dir)
70 66
71 def dump_host(self, dump_dir=""): 67 def _write_dump(self, command, output):
72 if dump_dir: 68 fullname = self._construct_filename(command)
73 self.dump_dir = dump_dir 69 os.makedirs(os.path.dirname(fullname), exist_ok=True)
74 env = os.environ.copy() 70 if isinstance(self, MonitorDumper):
75 env['PATH'] = '/usr/sbin:/sbin:/usr/bin:/bin' 71 with open(fullname, 'w') as json_file:
76 env['COLUMNS'] = '9999' 72 json.dump(output, json_file, indent=4)
77 for cmd in self.cmds: 73 else:
78 result = runCmd(cmd, ignore_status=True, env=env) 74 with open(fullname, 'w') as dump_file:
79 self._write_dump(cmd.split()[0], result.output) 75 dump_file.write(output)
80 76
81class TargetDumper(BaseDumper): 77class TargetDumper(BaseDumper):
82 """ Class to get dumps from target, it only works with QemuRunner """ 78 """ Class to get dumps from target, it only works with QemuRunner.
79 Will give up permanently after 5 errors from running commands over
80 serial console. This helps to end testing when target is really dead, hanging
81 or unresponsive.
82 """
83 83
84 def __init__(self, cmds, parent_dir, runner): 84 def __init__(self, cmds, parent_dir, runner):
85 super(TargetDumper, self).__init__(cmds, parent_dir) 85 super(TargetDumper, self).__init__(cmds, parent_dir)
86 self.runner = runner 86 self.runner = runner
87 self.errors = 0
87 88
88 def dump_target(self, dump_dir=""): 89 def dump_target(self, dump_dir=""):
90 if self.errors >= 5:
91 print("Too many errors when dumping data from target, assuming it is dead! Will not dump data anymore!")
92 return
89 if dump_dir: 93 if dump_dir:
90 self.dump_dir = dump_dir 94 self.dump_dir = dump_dir
91 for cmd in self.cmds: 95 for cmd in self.cmds:
92 # We can continue with the testing if serial commands fail 96 # We can continue with the testing if serial commands fail
93 try: 97 try:
94 (status, output) = self.runner.run_serial(cmd) 98 (status, output) = self.runner.run_serial(cmd)
99 if status == 0:
100 self.errors = self.errors + 1
95 self._write_dump(cmd.split()[0], output) 101 self._write_dump(cmd.split()[0], output)
96 except: 102 except:
103 self.errors = self.errors + 1
97 print("Tried to dump info from target but " 104 print("Tried to dump info from target but "
98 "serial console failed") 105 "serial console failed")
106 print("Failed CMD: %s" % (cmd))
107
108class MonitorDumper(BaseDumper):
109 """ Class to get dumps via the Qemu Monitor, it only works with QemuRunner
110 Will stop completely if there are more than 5 errors when dumping monitor data.
111 This helps to end testing when target is really dead, hanging or unresponsive.
112 """
113
114 def __init__(self, cmds, parent_dir, runner):
115 super(MonitorDumper, self).__init__(cmds, parent_dir)
116 self.runner = runner
117 self.errors = 0
118
119 def dump_monitor(self, dump_dir=""):
120 if self.runner is None:
121 return
122 if dump_dir:
123 self.dump_dir = dump_dir
124 if self.errors >= 5:
125 print("Too many errors when dumping data from qemu monitor, assuming it is dead! Will not dump data anymore!")
126 return
127 for cmd in self.cmds:
128 cmd_name = cmd.split()[0]
129 try:
130 if len(cmd.split()) > 1:
131 cmd_args = cmd.split()[1]
132 if "%s" in cmd_args:
133 filename = self._construct_filename(cmd_name)
134 cmd_data = json.loads(cmd_args % (filename))
135 output = self.runner.run_monitor(cmd_name, cmd_data)
136 else:
137 output = self.runner.run_monitor(cmd_name)
138 self._write_dump(cmd_name, output)
139 except Exception as e:
140 self.errors = self.errors + 1
141 print("Failed to dump QMP CMD: %s with\nException: %s" % (cmd_name, e))
diff --git a/meta/lib/oeqa/utils/ftools.py b/meta/lib/oeqa/utils/ftools.py
index 3093419cc7..a50aaa84c2 100644
--- a/meta/lib/oeqa/utils/ftools.py
+++ b/meta/lib/oeqa/utils/ftools.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
diff --git a/meta/lib/oeqa/utils/gitarchive.py b/meta/lib/oeqa/utils/gitarchive.py
index 6e8040eb5c..7e1d505748 100644
--- a/meta/lib/oeqa/utils/gitarchive.py
+++ b/meta/lib/oeqa/utils/gitarchive.py
@@ -67,7 +67,7 @@ def git_commit_data(repo, data_dir, branch, message, exclude, notes, log):
67 67
68 # Remove files that are excluded 68 # Remove files that are excluded
69 if exclude: 69 if exclude:
70 repo.run_cmd(['rm', '--cached'] + [f for f in exclude], env_update) 70 repo.run_cmd(['rm', '--cached', '--ignore-unmatch'] + [f for f in exclude], env_update)
71 71
72 tree = repo.run_cmd('write-tree', env_update) 72 tree = repo.run_cmd('write-tree', env_update)
73 73
@@ -100,9 +100,44 @@ def git_commit_data(repo, data_dir, branch, message, exclude, notes, log):
100 if os.path.exists(tmp_index): 100 if os.path.exists(tmp_index):
101 os.unlink(tmp_index) 101 os.unlink(tmp_index)
102 102
103def get_tags(repo, log, pattern=None, url=None):
104 """ Fetch remote tags from current repository
105
106 A pattern can be provided to filter returned tags list
107 An URL can be provided if local repository has no valid remote configured
108 """
109
110 base_cmd = ['ls-remote', '--refs', '--tags', '-q']
111 cmd = base_cmd.copy()
112
113 # First try to fetch tags from repository configured remote
114 cmd.append('origin')
115 if pattern:
116 cmd.append("refs/tags/"+pattern)
117 try:
118 tags_refs = repo.run_cmd(cmd)
119 tags = ["".join(d.split()[1].split('/', 2)[2:]) for d in tags_refs.splitlines()]
120 except GitError as e:
121 # If it fails, retry with repository url if one is provided
122 if url:
123 log.info("No remote repository configured, use provided url")
124 cmd = base_cmd.copy()
125 cmd.append(url)
126 if pattern:
127 cmd.append(pattern)
128 tags_refs = repo.run_cmd(cmd)
129 tags = ["".join(d.split()[1].split('/', 2)[2:]) for d in tags_refs.splitlines()]
130 else:
131 log.info("Read local tags only, some remote tags may be missed")
132 cmd = ["tag"]
133 if pattern:
134 cmd += ["-l", pattern]
135 tags = repo.run_cmd(cmd).splitlines()
136
137 return tags
103 138
104def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern, 139def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern,
105 keywords): 140 url, log, keywords):
106 """Generate tag name and message, with support for running id number""" 141 """Generate tag name and message, with support for running id number"""
107 keyws = keywords.copy() 142 keyws = keywords.copy()
108 # Tag number is handled specially: if not defined, we autoincrement it 143 # Tag number is handled specially: if not defined, we autoincrement it
@@ -111,12 +146,12 @@ def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern,
111 keyws['tag_number'] = '{tag_number}' 146 keyws['tag_number'] = '{tag_number}'
112 tag_re = format_str(name_pattern, keyws) 147 tag_re = format_str(name_pattern, keyws)
113 # Replace parentheses for proper regex matching 148 # Replace parentheses for proper regex matching
114 tag_re = tag_re.replace('(', '\(').replace(')', '\)') + '$' 149 tag_re = tag_re.replace('(', r'\(').replace(')', r'\)') + '$'
115 # Inject regex group pattern for 'tag_number' 150 # Inject regex group pattern for 'tag_number'
116 tag_re = tag_re.format(tag_number='(?P<tag_number>[0-9]{1,5})') 151 tag_re = tag_re.format(tag_number='(?P<tag_number>[0-9]{1,5})')
117 152
118 keyws['tag_number'] = 0 153 keyws['tag_number'] = 0
119 for existing_tag in repo.run_cmd('tag').splitlines(): 154 for existing_tag in get_tags(repo, log, url=url):
120 match = re.match(tag_re, existing_tag) 155 match = re.match(tag_re, existing_tag)
121 156
122 if match and int(match.group('tag_number')) >= keyws['tag_number']: 157 if match and int(match.group('tag_number')) >= keyws['tag_number']:
@@ -143,7 +178,8 @@ def gitarchive(data_dir, git_dir, no_create, bare, commit_msg_subject, commit_ms
143 if not no_tag and tagname: 178 if not no_tag and tagname:
144 tag_name, tag_msg = expand_tag_strings(data_repo, tagname, 179 tag_name, tag_msg = expand_tag_strings(data_repo, tagname,
145 tag_msg_subject, 180 tag_msg_subject,
146 tag_msg_body, keywords) 181 tag_msg_body,
182 push, log, keywords)
147 183
148 # Commit data 184 # Commit data
149 commit = git_commit_data(data_repo, data_dir, branch_name, 185 commit = git_commit_data(data_repo, data_dir, branch_name,
@@ -166,6 +202,8 @@ def gitarchive(data_dir, git_dir, no_create, bare, commit_msg_subject, commit_ms
166 log.info("Pushing data to remote") 202 log.info("Pushing data to remote")
167 data_repo.run_cmd(cmd) 203 data_repo.run_cmd(cmd)
168 204
205 return tag_name
206
169# Container class for tester revisions 207# Container class for tester revisions
170TestedRev = namedtuple('TestedRev', 'commit commit_number tags') 208TestedRev = namedtuple('TestedRev', 'commit commit_number tags')
171 209
@@ -181,7 +219,7 @@ def get_test_runs(log, repo, tag_name, **kwargs):
181 219
182 # Get a list of all matching tags 220 # Get a list of all matching tags
183 tag_pattern = tag_name.format(**str_fields) 221 tag_pattern = tag_name.format(**str_fields)
184 tags = repo.run_cmd(['tag', '-l', tag_pattern]).splitlines() 222 tags = get_tags(repo, log, pattern=tag_pattern)
185 log.debug("Found %d tags matching pattern '%s'", len(tags), tag_pattern) 223 log.debug("Found %d tags matching pattern '%s'", len(tags), tag_pattern)
186 224
187 # Parse undefined fields from tag names 225 # Parse undefined fields from tag names
@@ -199,6 +237,8 @@ def get_test_runs(log, repo, tag_name, **kwargs):
199 revs = [] 237 revs = []
200 for tag in tags: 238 for tag in tags:
201 m = tag_re.match(tag) 239 m = tag_re.match(tag)
240 if not m:
241 continue
202 groups = m.groupdict() 242 groups = m.groupdict()
203 revs.append([groups[f] for f in undef_fields] + [tag]) 243 revs.append([groups[f] for f in undef_fields] + [tag])
204 244
@@ -219,7 +259,15 @@ def get_test_revs(log, repo, tag_name, **kwargs):
219 if not commit in revs: 259 if not commit in revs:
220 revs[commit] = TestedRev(commit, commit_num, [tag]) 260 revs[commit] = TestedRev(commit, commit_num, [tag])
221 else: 261 else:
222 assert commit_num == revs[commit].commit_number, "Commit numbers do not match" 262 if commit_num != revs[commit].commit_number:
263 # Historically we have incorrect commit counts of '1' in the repo so fix these up
264 if int(revs[commit].commit_number) < 5:
265 tags = revs[commit].tags
266 revs[commit] = TestedRev(commit, commit_num, [tags])
267 elif int(commit_num) < 5:
268 pass
269 else:
270 sys.exit("Commit numbers for commit %s don't match (%s vs %s)" % (commit, commit_num, revs[commit].commit_number))
223 revs[commit].tags.append(tag) 271 revs[commit].tags.append(tag)
224 272
225 # Return in sorted table 273 # Return in sorted table
diff --git a/meta/lib/oeqa/utils/httpserver.py b/meta/lib/oeqa/utils/httpserver.py
index 58d3c3b3f8..80752c1377 100644
--- a/meta/lib/oeqa/utils/httpserver.py
+++ b/meta/lib/oeqa/utils/httpserver.py
@@ -1,11 +1,13 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
5import http.server 7import http.server
8import logging
6import multiprocessing 9import multiprocessing
7import os 10import os
8import traceback
9import signal 11import signal
10from socketserver import ThreadingMixIn 12from socketserver import ThreadingMixIn
11 13
@@ -13,20 +15,24 @@ class HTTPServer(ThreadingMixIn, http.server.HTTPServer):
13 15
14 def server_start(self, root_dir, logger): 16 def server_start(self, root_dir, logger):
15 os.chdir(root_dir) 17 os.chdir(root_dir)
18 self.logger = logger
16 self.serve_forever() 19 self.serve_forever()
17 20
18class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 21class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
19 22
20 def log_message(self, format_str, *args): 23 def log_message(self, format_str, *args):
21 pass 24 self.server.logger.info(format_str, *args)
22 25
23class HTTPService(object): 26class HTTPService:
24 27
25 def __init__(self, root_dir, host='', port=0, logger=None): 28 def __init__(self, root_dir, host='', port=0, logger=None):
26 self.root_dir = root_dir 29 self.root_dir = root_dir
27 self.host = host 30 self.host = host
28 self.port = port 31 self.port = port
29 self.logger = logger 32 if logger:
33 self.logger = logger.getChild("HTTPService")
34 else:
35 self.logger = logging.getLogger("HTTPService")
30 36
31 def start(self): 37 def start(self):
32 if not os.path.exists(self.root_dir): 38 if not os.path.exists(self.root_dir):
@@ -38,6 +44,12 @@ class HTTPService(object):
38 self.port = self.server.server_port 44 self.port = self.server.server_port
39 self.process = multiprocessing.Process(target=self.server.server_start, args=[self.root_dir, self.logger]) 45 self.process = multiprocessing.Process(target=self.server.server_start, args=[self.root_dir, self.logger])
40 46
47 def handle_error(self, request, client_address):
48 import traceback
49 exception = traceback.format_exc()
50 self.logger.warn("Exception when handling %s: %s" % (request, exception))
51 self.server.handle_error = handle_error
52
41 # The signal handler from testimage.bbclass can cause deadlocks here 53 # The signal handler from testimage.bbclass can cause deadlocks here
42 # if the HTTPServer is terminated before it can restore the standard 54 # if the HTTPServer is terminated before it can restore the standard
43 #signal behaviour 55 #signal behaviour
@@ -47,7 +59,7 @@ class HTTPService(object):
47 signal.signal(signal.SIGTERM, orig) 59 signal.signal(signal.SIGTERM, orig)
48 60
49 if self.logger: 61 if self.logger:
50 self.logger.info("Started HTTPService on %s:%s" % (self.host, self.port)) 62 self.logger.info("Started HTTPService for %s on %s:%s" % (self.root_dir, self.host, self.port))
51 63
52 64
53 def stop(self): 65 def stop(self):
@@ -59,3 +71,10 @@ class HTTPService(object):
59 if self.logger: 71 if self.logger:
60 self.logger.info("Stopped HTTPService on %s:%s" % (self.host, self.port)) 72 self.logger.info("Stopped HTTPService on %s:%s" % (self.host, self.port))
61 73
74if __name__ == "__main__":
75 import sys, logging
76
77 logger = logging.getLogger(__name__)
78 logging.basicConfig(level=logging.DEBUG)
79 httpd = HTTPService(sys.argv[1], port=8888, logger=logger)
80 httpd.start()
diff --git a/meta/lib/oeqa/utils/logparser.py b/meta/lib/oeqa/utils/logparser.py
index 60e16d500e..496d9e0c90 100644
--- a/meta/lib/oeqa/utils/logparser.py
+++ b/meta/lib/oeqa/utils/logparser.py
@@ -1,8 +1,10 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
5import sys 7import enum
6import os 8import os
7import re 9import re
8 10
@@ -42,6 +44,8 @@ class PtestParser(object):
42 result = section_regex['begin'].search(line) 44 result = section_regex['begin'].search(line)
43 if result: 45 if result:
44 current_section['name'] = result.group(1) 46 current_section['name'] = result.group(1)
47 if current_section['name'] not in self.results:
48 self.results[current_section['name']] = {}
45 continue 49 continue
46 50
47 result = section_regex['end'].search(line) 51 result = section_regex['end'].search(line)
@@ -73,9 +77,10 @@ class PtestParser(object):
73 for t in test_regex: 77 for t in test_regex:
74 result = test_regex[t].search(line) 78 result = test_regex[t].search(line)
75 if result: 79 if result:
76 if current_section['name'] not in self.results: 80 try:
77 self.results[current_section['name']] = {} 81 self.results[current_section['name']][result.group(1).strip()] = t
78 self.results[current_section['name']][result.group(1).strip()] = t 82 except KeyError:
83 bb.warn("Result with no section: %s - %s" % (t, result.group(1).strip()))
79 84
80 # Python performance for repeatedly joining long strings is poor, do it all at once at the end. 85 # Python performance for repeatedly joining long strings is poor, do it all at once at the end.
81 # For 2.1 million lines in a log this reduces 18 hours to 12s. 86 # For 2.1 million lines in a log this reduces 18 hours to 12s.
@@ -101,30 +106,48 @@ class PtestParser(object):
101 f.write(status + ": " + test_name + "\n") 106 f.write(status + ": " + test_name + "\n")
102 107
103 108
104# ltp log parsing 109class LtpParser:
105class LtpParser(object): 110 """
106 def __init__(self): 111 Parse the machine-readable LTP log output into a ptest-friendly data structure.
107 self.results = {} 112 """
108 self.section = {'duration': "", 'log': ""}
109
110 def parse(self, logfile): 113 def parse(self, logfile):
111 test_regex = {} 114 results = {}
112 test_regex['PASSED'] = re.compile(r"PASS") 115 # Aaccumulate the duration here but as the log rounds quick tests down
113 test_regex['FAILED'] = re.compile(r"FAIL") 116 # to 0 seconds this is very much a lower bound. The caller can replace
114 test_regex['SKIPPED'] = re.compile(r"SKIP") 117 # the value.
115 118 section = {"duration": 0, "log": ""}
116 with open(logfile, errors='replace') as f: 119
120 class LtpExitCode(enum.IntEnum):
121 # Exit codes as defined in ltp/include/tst_res_flags.h
122 TPASS = 0 # Test passed flag
123 TFAIL = 1 # Test failed flag
124 TBROK = 2 # Test broken flag
125 TWARN = 4 # Test warning flag
126 TINFO = 16 # Test information flag
127 TCONF = 32 # Test not appropriate for configuration flag
128
129 with open(logfile, errors="replace") as f:
130 # Lines look like this:
131 # tag=cfs_bandwidth01 stime=1689762564 dur=0 exit=exited stat=32 core=no cu=0 cs=0
117 for line in f: 132 for line in f:
118 for t in test_regex: 133 if not line.startswith("tag="):
119 result = test_regex[t].search(line) 134 continue
120 if result:
121 self.results[line.split()[0].strip()] = t
122 135
123 for test in self.results: 136 values = dict(s.split("=") for s in line.strip().split())
124 result = self.results[test]
125 self.section['log'] = self.section['log'] + ("%s: %s\n" % (result.strip()[:-2], test.strip()))
126 137
127 return self.results, self.section 138 section["duration"] += int(values["dur"])
139 exitcode = int(values["stat"])
140 if values["exit"] == "exited" and exitcode == LtpExitCode.TCONF:
141 # Exited normally with the "invalid configuration" code
142 results[values["tag"]] = "SKIPPED"
143 elif exitcode == LtpExitCode.TPASS:
144 # Successful exit
145 results[values["tag"]] = "PASSED"
146 else:
147 # Other exit
148 results[values["tag"]] = "FAILED"
149
150 return results, section
128 151
129 152
130# ltp Compliance log parsing 153# ltp Compliance log parsing
@@ -135,30 +158,27 @@ class LtpComplianceParser(object):
135 158
136 def parse(self, logfile): 159 def parse(self, logfile):
137 test_regex = {} 160 test_regex = {}
138 test_regex['PASSED'] = re.compile(r"^PASS") 161 test_regex['FAILED'] = re.compile(r"FAIL")
139 test_regex['FAILED'] = re.compile(r"^FAIL")
140 test_regex['SKIPPED'] = re.compile(r"(?:UNTESTED)|(?:UNSUPPORTED)")
141 162
142 section_regex = {} 163 section_regex = {}
143 section_regex['test'] = re.compile(r"^Testing") 164 section_regex['test'] = re.compile(r"^Executing")
144 165
145 with open(logfile, errors='replace') as f: 166 with open(logfile, errors='replace') as f:
167 name = logfile
168 result = "PASSED"
146 for line in f: 169 for line in f:
147 result = section_regex['test'].search(line) 170 regex_result = section_regex['test'].search(line)
148 if result: 171 if regex_result:
149 self.name = "" 172 name = line.split()[1].strip()
150 self.name = line.split()[1].strip()
151 self.results[self.name] = "PASSED"
152 failed = 0
153 173
154 failed_result = test_regex['FAILED'].search(line) 174 regex_result = test_regex['FAILED'].search(line)
155 if failed_result: 175 if regex_result:
156 failed = line.split()[1].strip() 176 result = "FAILED"
157 if int(failed) > 0: 177 self.results[name] = result
158 self.results[self.name] = "FAILED"
159 178
160 for test in self.results: 179 for test in self.results:
161 result = self.results[test] 180 result = self.results[test]
181 print (self.results)
162 self.section['log'] = self.section['log'] + ("%s: %s\n" % (result.strip()[:-2], test.strip())) 182 self.section['log'] = self.section['log'] + ("%s: %s\n" % (result.strip()[:-2], test.strip()))
163 183
164 return self.results, self.section 184 return self.results, self.section
diff --git a/meta/lib/oeqa/utils/metadata.py b/meta/lib/oeqa/utils/metadata.py
index 8013aa684d..b320df67e0 100644
--- a/meta/lib/oeqa/utils/metadata.py
+++ b/meta/lib/oeqa/utils/metadata.py
@@ -27,9 +27,9 @@ def metadata_from_bb():
27 data_dict = get_bb_vars() 27 data_dict = get_bb_vars()
28 28
29 # Distro information 29 # Distro information
30 info_dict['distro'] = {'id': data_dict['DISTRO'], 30 info_dict['distro'] = {'id': data_dict.get('DISTRO', 'NODISTRO'),
31 'version_id': data_dict['DISTRO_VERSION'], 31 'version_id': data_dict.get('DISTRO_VERSION', 'NO_DISTRO_VERSION'),
32 'pretty_name': '%s %s' % (data_dict['DISTRO'], data_dict['DISTRO_VERSION'])} 32 'pretty_name': '%s %s' % (data_dict.get('DISTRO', 'NODISTRO'), data_dict.get('DISTRO_VERSION', 'NO_DISTRO_VERSION'))}
33 33
34 # Host distro information 34 # Host distro information
35 os_release = get_os_release() 35 os_release = get_os_release()
@@ -76,6 +76,10 @@ def git_rev_info(path):
76 info['commit_count'] = int(subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=path).decode('utf-8').strip()) 76 info['commit_count'] = int(subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=path).decode('utf-8').strip())
77 except subprocess.CalledProcessError: 77 except subprocess.CalledProcessError:
78 pass 78 pass
79 try:
80 info['commit_time'] = int(subprocess.check_output(["git", "show", "--no-patch", "--format=%ct", "HEAD"], cwd=path).decode('utf-8').strip())
81 except subprocess.CalledProcessError:
82 pass
79 return info 83 return info
80 try: 84 try:
81 repo = Repo(path, search_parent_directories=True) 85 repo = Repo(path, search_parent_directories=True)
@@ -83,6 +87,7 @@ def git_rev_info(path):
83 return info 87 return info
84 info['commit'] = repo.head.commit.hexsha 88 info['commit'] = repo.head.commit.hexsha
85 info['commit_count'] = repo.head.commit.count() 89 info['commit_count'] = repo.head.commit.count()
90 info['commit_time'] = repo.head.commit.committed_date
86 try: 91 try:
87 info['branch'] = repo.active_branch.name 92 info['branch'] = repo.active_branch.name
88 except TypeError: 93 except TypeError:
diff --git a/meta/lib/oeqa/utils/network.py b/meta/lib/oeqa/utils/network.py
index 59d01723a1..da4ffda9a9 100644
--- a/meta/lib/oeqa/utils/network.py
+++ b/meta/lib/oeqa/utils/network.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
diff --git a/meta/lib/oeqa/utils/nfs.py b/meta/lib/oeqa/utils/nfs.py
index a37686c914..903469bfee 100644
--- a/meta/lib/oeqa/utils/nfs.py
+++ b/meta/lib/oeqa/utils/nfs.py
@@ -1,4 +1,8 @@
1#
2# Copyright OpenEmbedded Contributors
3#
1# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
5#
2import os 6import os
3import sys 7import sys
4import tempfile 8import tempfile
@@ -8,7 +12,7 @@ from oeqa.utils.commands import bitbake, get_bb_var, Command
8from oeqa.utils.network import get_free_port 12from oeqa.utils.network import get_free_port
9 13
10@contextlib.contextmanager 14@contextlib.contextmanager
11def unfs_server(directory, logger = None): 15def unfs_server(directory, logger = None, udp = True):
12 unfs_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "unfs3-native") 16 unfs_sysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "unfs3-native")
13 if not os.path.exists(os.path.join(unfs_sysroot, "usr", "bin", "unfsd")): 17 if not os.path.exists(os.path.join(unfs_sysroot, "usr", "bin", "unfsd")):
14 # build native tool 18 # build native tool
@@ -22,11 +26,11 @@ def unfs_server(directory, logger = None):
22 exports.write("{0} (rw,no_root_squash,no_all_squash,insecure)\n".format(directory).encode()) 26 exports.write("{0} (rw,no_root_squash,no_all_squash,insecure)\n".format(directory).encode())
23 27
24 # find some ports for the server 28 # find some ports for the server
25 nfsport, mountport = get_free_port(udp = True), get_free_port(udp = True) 29 nfsport, mountport = get_free_port(udp), get_free_port(udp)
26 30
27 nenv = dict(os.environ) 31 nenv = dict(os.environ)
28 nenv['PATH'] = "{0}/sbin:{0}/usr/sbin:{0}/usr/bin:".format(unfs_sysroot) + nenv.get('PATH', '') 32 nenv['PATH'] = "{0}/sbin:{0}/usr/sbin:{0}/usr/bin:".format(unfs_sysroot) + nenv.get('PATH', '')
29 cmd = Command(["unfsd", "-d", "-p", "-N", "-e", exports.name, "-n", str(nfsport), "-m", str(mountport)], 33 cmd = Command(["unfsd", "-d", "-p", "-e", exports.name, "-n", str(nfsport), "-m", str(mountport)],
30 bg = True, env = nenv, output_log = logger) 34 bg = True, env = nenv, output_log = logger)
31 cmd.run() 35 cmd.run()
32 yield nfsport, mountport 36 yield nfsport, mountport
diff --git a/meta/lib/oeqa/utils/package_manager.py b/meta/lib/oeqa/utils/package_manager.py
index 6b67f22fdd..db799b64d6 100644
--- a/meta/lib/oeqa/utils/package_manager.py
+++ b/meta/lib/oeqa/utils/package_manager.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4 6
diff --git a/meta/lib/oeqa/utils/postactions.py b/meta/lib/oeqa/utils/postactions.py
new file mode 100644
index 0000000000..c69481db6c
--- /dev/null
+++ b/meta/lib/oeqa/utils/postactions.py
@@ -0,0 +1,102 @@
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7# Run a set of actions after tests. The runner provides internal data
8# dictionary as well as test context to any action to run.
9
10import datetime
11import io
12import os
13import stat
14import subprocess
15import tempfile
16from oeqa.utils import get_artefact_dir
17
18##################################################################
19# Host/target statistics
20##################################################################
21
22def get_target_disk_usage(d, tc, artifacts_list, outputdir):
23 output_file = os.path.join(outputdir, "target_disk_usage.txt")
24 try:
25 (status, output) = tc.target.run('df -h')
26 with open(output_file, 'w') as f:
27 f.write(output)
28 f.write("\n")
29 except Exception as e:
30 bb.warn(f"Can not get target disk usage: {e}")
31
32def get_host_disk_usage(d, tc, artifacts_list, outputdir):
33 import subprocess
34
35 output_file = os.path.join(outputdir, "host_disk_usage.txt")
36 try:
37 with open(output_file, 'w') as f:
38 output = subprocess.run(['df', '-hl'], check=True, text=True, stdout=f, env={})
39 except Exception as e:
40 bb.warn(f"Can not get host disk usage: {e}")
41
42##################################################################
43# Artifacts retrieval
44##################################################################
45
46def get_artifacts_list(target, raw_list):
47 result = []
48 # Passed list may contains patterns in paths, expand them directly on target
49 for raw_path in raw_list.split():
50 cmd = f"for p in {raw_path}; do if [ -e $p ]; then echo $p; fi; done"
51 try:
52 status, output = target.run(cmd)
53 if status != 0 or not output:
54 raise Exception()
55 result += output.split()
56 except:
57 bb.note(f"No file/directory matching path {raw_path}")
58
59 return result
60
61def list_and_fetch_failed_tests_artifacts(d, tc, artifacts_list, outputdir):
62 artifacts_list = get_artifacts_list(tc.target, artifacts_list)
63 if not artifacts_list:
64 bb.warn("Could not load artifacts list, skip artifacts retrieval")
65 return
66 try:
67 # We need gnu tar for sparse files, not busybox
68 cmd = "tar --sparse -zcf - " + " ".join(artifacts_list)
69 (status, output) = tc.target.run(cmd, raw = True)
70 if status != 0 or not output:
71 raise Exception("Error while fetching compressed artifacts")
72 archive_name = os.path.join(outputdir, "tests_artifacts.tar.gz")
73 with open(archive_name, "wb") as f:
74 f.write(output)
75 except Exception as e:
76 bb.warn(f"Can not retrieve artifacts from test target: {e}")
77
78
79##################################################################
80# General post actions runner
81##################################################################
82
83def run_failed_tests_post_actions(d, tc):
84 artifacts = d.getVar("TESTIMAGE_FAILED_QA_ARTIFACTS")
85 # Allow all the code to be disabled by having no artifacts set, e.g. for systems with no ssh support
86 if not artifacts:
87 return
88
89 outputdir = get_artefact_dir(d)
90 os.makedirs(outputdir, exist_ok=True)
91 datestr = datetime.datetime.now().strftime('%Y%m%d')
92 outputdir = tempfile.mkdtemp(prefix='oeqa-target-artefacts-%s-' % datestr, dir=outputdir)
93 os.chmod(outputdir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
94
95 post_actions=[
96 list_and_fetch_failed_tests_artifacts,
97 get_target_disk_usage,
98 get_host_disk_usage
99 ]
100
101 for action in post_actions:
102 action(d, tc, artifacts, outputdir)
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py
index 77ec939ad7..c4db0cf038 100644
--- a/meta/lib/oeqa/utils/qemurunner.py
+++ b/meta/lib/oeqa/utils/qemurunner.py
@@ -19,20 +19,33 @@ import errno
19import string 19import string
20import threading 20import threading
21import codecs 21import codecs
22import logging 22import tempfile
23from oeqa.utils.dump import HostDumper
24from collections import defaultdict 23from collections import defaultdict
24from contextlib import contextmanager
25import importlib
26import traceback
25 27
26# Get Unicode non printable control chars 28# Get Unicode non printable control chars
27control_range = list(range(0,32))+list(range(127,160)) 29control_range = list(range(0,32))+list(range(127,160))
28control_chars = [chr(x) for x in control_range 30control_chars = [chr(x) for x in control_range
29 if chr(x) not in string.printable] 31 if chr(x) not in string.printable]
30re_control_char = re.compile('[%s]' % re.escape("".join(control_chars))) 32re_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
33# Regex to remove the ANSI (color) control codes from console strings in order to match the text only
34re_vt100 = re.compile(r'(\x1b\[|\x9b)[^@-_a-z]*[@-_a-z]|\x1b[@-_a-z]')
35
36def getOutput(o):
37 import fcntl
38 fl = fcntl.fcntl(o, fcntl.F_GETFL)
39 fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
40 try:
41 return os.read(o.fileno(), 1000000).decode("utf-8")
42 except BlockingIOError:
43 return ""
31 44
32class QemuRunner: 45class QemuRunner:
33 46
34 def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds, 47 def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, use_kvm, logger, use_slirp=False,
35 use_kvm, logger, use_slirp=False, serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None): 48 serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None, tmpfsdir=None):
36 49
37 # Popen object for runqemu 50 # Popen object for runqemu
38 self.runqemu = None 51 self.runqemu = None
@@ -55,21 +68,24 @@ class QemuRunner:
55 self.boottime = boottime 68 self.boottime = boottime
56 self.logged = False 69 self.logged = False
57 self.thread = None 70 self.thread = None
71 self.threadsock = None
58 self.use_kvm = use_kvm 72 self.use_kvm = use_kvm
59 self.use_ovmf = use_ovmf 73 self.use_ovmf = use_ovmf
60 self.use_slirp = use_slirp 74 self.use_slirp = use_slirp
61 self.serial_ports = serial_ports 75 self.serial_ports = serial_ports
62 self.msg = '' 76 self.msg = ''
63 self.boot_patterns = boot_patterns 77 self.boot_patterns = boot_patterns
78 self.tmpfsdir = tmpfsdir
64 79
65 self.runqemutime = 120 80 self.runqemutime = 300
66 if not workdir: 81 if not workdir:
67 workdir = os.getcwd() 82 workdir = os.getcwd()
68 self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid()) 83 self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid())
69 self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
70 self.monitorpipe = None 84 self.monitorpipe = None
71 85
72 self.logger = logger 86 self.logger = logger
87 # Whether we're expecting an exit and should show related errors
88 self.canexit = False
73 89
74 # Enable testing other OS's 90 # Enable testing other OS's
75 # Set commands for target communication, and default to Linux ALWAYS 91 # Set commands for target communication, and default to Linux ALWAYS
@@ -80,20 +96,21 @@ class QemuRunner:
80 accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished'] 96 accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished']
81 default_boot_patterns = defaultdict(str) 97 default_boot_patterns = defaultdict(str)
82 # Default to the usual paterns used to communicate with the target 98 # Default to the usual paterns used to communicate with the target
83 default_boot_patterns['search_reached_prompt'] = b' login:' 99 default_boot_patterns['search_reached_prompt'] = ' login:'
84 default_boot_patterns['send_login_user'] = 'root\n' 100 default_boot_patterns['send_login_user'] = 'root\n'
85 default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#" 101 default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#"
86 default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#" 102 default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
87 103
88 # Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n" 104 # Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n"
89 for pattern in accepted_patterns: 105 for pattern in accepted_patterns:
90 if not self.boot_patterns[pattern]: 106 if pattern not in self.boot_patterns or not self.boot_patterns[pattern]:
91 self.boot_patterns[pattern] = default_boot_patterns[pattern] 107 self.boot_patterns[pattern] = default_boot_patterns[pattern]
92 108
93 def create_socket(self): 109 def create_socket(self):
94 try: 110 try:
95 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 111 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
96 sock.setblocking(0) 112 sock.setblocking(0)
113 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
97 sock.bind(("127.0.0.1",0)) 114 sock.bind(("127.0.0.1",0))
98 sock.listen(2) 115 sock.listen(2)
99 port = sock.getsockname()[1] 116 port = sock.getsockname()[1]
@@ -104,30 +121,24 @@ class QemuRunner:
104 sock.close() 121 sock.close()
105 raise 122 raise
106 123
107 def log(self, msg): 124 def decode_qemulog(self, todecode):
108 if self.logfile: 125 # Sanitize the data received from qemu as it may contain control characters
109 # It is needed to sanitize the data received from qemu 126 msg = todecode.decode("utf-8", errors='backslashreplace')
110 # because is possible to have control characters 127 msg = re_control_char.sub('', msg)
111 msg = msg.decode("utf-8", errors='ignore') 128 return msg
112 msg = re_control_char.sub('', msg)
113 self.msg += msg
114 with codecs.open(self.logfile, "a", encoding="utf-8") as f:
115 f.write("%s" % msg)
116
117 def getOutput(self, o):
118 import fcntl
119 fl = fcntl.fcntl(o, fcntl.F_GETFL)
120 fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
121 return os.read(o.fileno(), 1000000).decode("utf-8")
122 129
130 def log(self, msg, extension=""):
131 if self.logfile:
132 with codecs.open(self.logfile + extension, "ab") as f:
133 f.write(msg)
134 self.msg += self.decode_qemulog(msg)
123 135
124 def handleSIGCHLD(self, signum, frame): 136 def handleSIGCHLD(self, signum, frame):
125 if self.runqemu and self.runqemu.poll(): 137 if self.runqemu and self.runqemu.poll():
126 if self.runqemu.returncode: 138 if self.runqemu.returncode:
127 self.logger.error('runqemu exited with code %d' % self.runqemu.returncode) 139 self.logger.error('runqemu exited with code %d' % self.runqemu.returncode)
128 self.logger.error('Output from runqemu:\n%s' % self.getOutput(self.runqemu.stdout)) 140 self.logger.error('Output from runqemu:\n%s' % getOutput(self.runqemu.stdout))
129 self.stop() 141 self.stop()
130 self._dump_host()
131 142
132 def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True): 143 def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
133 env = os.environ.copy() 144 env = os.environ.copy()
@@ -150,6 +161,9 @@ class QemuRunner:
150 else: 161 else:
151 env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image 162 env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
152 163
164 if self.tmpfsdir:
165 env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
166
153 if not launch_cmd: 167 if not launch_cmd:
154 launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '') 168 launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '')
155 if self.use_kvm: 169 if self.use_kvm:
@@ -163,11 +177,38 @@ class QemuRunner:
163 launch_cmd += ' slirp' 177 launch_cmd += ' slirp'
164 if self.use_ovmf: 178 if self.use_ovmf:
165 launch_cmd += ' ovmf' 179 launch_cmd += ' ovmf'
166 launch_cmd += ' %s %s %s' % (runqemuparams, self.machine, self.rootfs) 180 launch_cmd += ' %s %s' % (runqemuparams, self.machine)
181 if self.rootfs.endswith('.vmdk'):
182 self.logger.debug('Bypassing VMDK rootfs for runqemu')
183 else:
184 launch_cmd += ' %s' % (self.rootfs)
167 185
168 return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env) 186 return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
169 187
170 def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None): 188 def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
189 # use logfile to determine the recipe-sysroot-native path and
190 # then add in the site-packages path components and add that
191 # to the python sys.path so the qmp module can be found.
192 python_path = os.path.dirname(os.path.dirname(self.logfile))
193 python_path += "/recipe-sysroot-native/usr/lib/qemu-python"
194 sys.path.append(python_path)
195 importlib.invalidate_caches()
196 try:
197 qmp = importlib.import_module("qmp")
198 except Exception as e:
199 self.logger.error("qemurunner: qmp module missing, please ensure it's installed in %s (%s)" % (python_path, str(e)))
200 return False
201 # Path relative to tmpdir used as cwd for qemu below to avoid unix socket path length issues
202 qmp_file = "." + next(tempfile._get_candidate_names())
203 qmp_param = ' -S -qmp unix:./%s,server,wait' % (qmp_file)
204 qmp_port = self.tmpdir + "/" + qmp_file
205 # Create a second socket connection for debugging use,
206 # note this will NOT cause qemu to block waiting for the connection
207 qmp_file2 = "." + next(tempfile._get_candidate_names())
208 qmp_param += ' -qmp unix:./%s,server,nowait' % (qmp_file2)
209 qmp_port2 = self.tmpdir + "/" + qmp_file2
210 self.logger.info("QMP Available for connection at %s" % (qmp_port2))
211
171 try: 212 try:
172 if self.serial_ports >= 2: 213 if self.serial_ports >= 2:
173 self.threadsock, threadport = self.create_socket() 214 self.threadsock, threadport = self.create_socket()
@@ -176,7 +217,7 @@ class QemuRunner:
176 self.logger.error("Failed to create listening socket: %s" % msg[1]) 217 self.logger.error("Failed to create listening socket: %s" % msg[1])
177 return False 218 return False
178 219
179 bootparams = 'console=tty1 console=ttyS0,115200n8 printk.time=1' 220 bootparams = ' printk.time=1'
180 if extra_bootparams: 221 if extra_bootparams:
181 bootparams = bootparams + ' ' + extra_bootparams 222 bootparams = bootparams + ' ' + extra_bootparams
182 223
@@ -184,7 +225,8 @@ class QemuRunner:
184 # and analyze descendents in order to determine it. 225 # and analyze descendents in order to determine it.
185 if os.path.exists(self.qemu_pidfile): 226 if os.path.exists(self.qemu_pidfile):
186 os.remove(self.qemu_pidfile) 227 os.remove(self.qemu_pidfile)
187 self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile) 228 self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
229
188 if qemuparams: 230 if qemuparams:
189 self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"' 231 self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
190 232
@@ -196,14 +238,15 @@ class QemuRunner:
196 self.origchldhandler = signal.getsignal(signal.SIGCHLD) 238 self.origchldhandler = signal.getsignal(signal.SIGCHLD)
197 signal.signal(signal.SIGCHLD, self.handleSIGCHLD) 239 signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
198 240
199 self.logger.debug('launchcmd=%s'%(launch_cmd)) 241 self.logger.debug('launchcmd=%s' % (launch_cmd))
200 242
201 # FIXME: We pass in stdin=subprocess.PIPE here to work around stty 243 # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
202 # blocking at the end of the runqemu script when using this within 244 # blocking at the end of the runqemu script when using this within
203 # oe-selftest (this makes stty error out immediately). There ought 245 # oe-selftest (this makes stty error out immediately). There ought
204 # to be a proper fix but this will suffice for now. 246 # to be a proper fix but this will suffice for now.
205 self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp, env=env) 247 self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp, env=env, cwd=self.tmpdir)
206 output = self.runqemu.stdout 248 output = self.runqemu.stdout
249 launch_time = time.time()
207 250
208 # 251 #
209 # We need the preexec_fn above so that all runqemu processes can easily be killed 252 # We need the preexec_fn above so that all runqemu processes can easily be killed
@@ -224,35 +267,41 @@ class QemuRunner:
224 self.monitorpipe = os.fdopen(w, "w") 267 self.monitorpipe = os.fdopen(w, "w")
225 else: 268 else:
226 # child process 269 # child process
227 os.setpgrp() 270 try:
228 os.close(w) 271 os.setpgrp()
229 r = os.fdopen(r) 272 os.close(w)
230 x = r.read() 273 r = os.fdopen(r)
231 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM) 274 x = r.read()
232 sys.exit(0) 275 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
276 finally:
277 # We must exit under all circumstances
278 os._exit(0)
233 279
234 self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid) 280 self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
235 self.logger.debug("waiting at most %s seconds for qemu pid (%s)" % 281 self.logger.debug("waiting at most %d seconds for qemu pid (%s)" %
236 (self.runqemutime, time.strftime("%D %H:%M:%S"))) 282 (self.runqemutime, time.strftime("%D %H:%M:%S")))
237 endtime = time.time() + self.runqemutime 283 endtime = time.time() + self.runqemutime
238 while not self.is_alive() and time.time() < endtime: 284 while not self.is_alive() and time.time() < endtime:
239 if self.runqemu.poll(): 285 if self.runqemu.poll():
240 if self.runqemu_exited: 286 if self.runqemu_exited:
287 self.logger.warning("runqemu during is_alive() test")
241 return False 288 return False
242 if self.runqemu.returncode: 289 if self.runqemu.returncode:
243 # No point waiting any longer 290 # No point waiting any longer
244 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode) 291 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
245 self._dump_host() 292 self.logger.warning("Output from runqemu:\n%s" % getOutput(output))
246 self.logger.warning("Output from runqemu:\n%s" % self.getOutput(output))
247 self.stop() 293 self.stop()
248 return False 294 return False
249 time.sleep(0.5) 295 time.sleep(0.5)
250 296
251 if self.runqemu_exited: 297 if self.runqemu_exited:
252 return False 298 self.logger.warning("runqemu after timeout")
299
300 if self.runqemu.returncode:
301 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
253 302
254 if not self.is_alive(): 303 if not self.is_alive():
255 self.logger.error("Qemu pid didn't appear in %s seconds (%s)" % 304 self.logger.error("Qemu pid didn't appear in %d seconds (%s)" %
256 (self.runqemutime, time.strftime("%D %H:%M:%S"))) 305 (self.runqemutime, time.strftime("%D %H:%M:%S")))
257 306
258 qemu_pid = None 307 qemu_pid = None
@@ -267,8 +316,7 @@ class QemuRunner:
267 ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0] 316 ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0]
268 processes = ps.decode("utf-8") 317 processes = ps.decode("utf-8")
269 self.logger.debug("Running processes:\n%s" % processes) 318 self.logger.debug("Running processes:\n%s" % processes)
270 self._dump_host() 319 op = getOutput(output)
271 op = self.getOutput(output)
272 self.stop() 320 self.stop()
273 if op: 321 if op:
274 self.logger.error("Output from runqemu:\n%s" % op) 322 self.logger.error("Output from runqemu:\n%s" % op)
@@ -276,10 +324,79 @@ class QemuRunner:
276 self.logger.error("No output from runqemu.\n") 324 self.logger.error("No output from runqemu.\n")
277 return False 325 return False
278 326
327 # Create the client socket for the QEMU Monitor Control Socket
328 # This will allow us to read status from Qemu if the the process
329 # is still alive
330 self.logger.debug("QMP Initializing to %s" % (qmp_port))
331 # chdir dance for path length issues with unix sockets
332 origpath = os.getcwd()
333 try:
334 os.chdir(os.path.dirname(qmp_port))
335 try:
336 from qmp.legacy import QEMUMonitorProtocol
337 self.qmp = QEMUMonitorProtocol(os.path.basename(qmp_port))
338 except OSError as msg:
339 self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
340 return False
341
342 self.logger.debug("QMP Connecting to %s" % (qmp_port))
343 if not os.path.exists(qmp_port) and self.is_alive():
344 self.logger.debug("QMP Port does not exist waiting for it to be created")
345 endtime = time.time() + self.runqemutime
346 while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
347 self.logger.info("QMP port does not exist yet!")
348 time.sleep(0.5)
349 if not os.path.exists(qmp_port) and self.is_alive():
350 self.logger.warning("QMP Port still does not exist but QEMU is alive")
351 return False
352
353 try:
354 # set timeout value for all QMP calls
355 self.qmp.settimeout(self.runqemutime)
356 self.qmp.connect()
357 connect_time = time.time()
358 self.logger.info("QMP connected to QEMU at %s and took %.2f seconds" %
359 (time.strftime("%D %H:%M:%S"),
360 time.time() - launch_time))
361 except OSError as msg:
362 self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
363 return False
364 except qmp.legacy.QMPError as msg:
365 self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
366 return False
367 finally:
368 os.chdir(origpath)
369
370 # We worry that mmap'd libraries may cause page faults which hang the qemu VM for periods
371 # causing failures. Before we "start" qemu, read through it's mapped files to try and
372 # ensure we don't hit page faults later
373 mapdir = "/proc/" + str(self.qemupid) + "/map_files/"
374 try:
375 for f in os.listdir(mapdir):
376 try:
377 linktarget = os.readlink(os.path.join(mapdir, f))
378 if not linktarget.startswith("/") or linktarget.startswith("/dev") or "deleted" in linktarget:
379 continue
380 with open(linktarget, "rb") as readf:
381 data = True
382 while data:
383 data = readf.read(4096)
384 except FileNotFoundError:
385 continue
386 # Centos7 doesn't allow us to read /map_files/
387 except PermissionError:
388 pass
389
390 # Release the qemu process to continue running
391 self.run_monitor('cont')
392 self.logger.info("QMP released QEMU at %s and took %.2f seconds from connect" %
393 (time.strftime("%D %H:%M:%S"),
394 time.time() - connect_time))
395
279 # We are alive: qemu is running 396 # We are alive: qemu is running
280 out = self.getOutput(output) 397 out = getOutput(output)
281 netconf = False # network configuration is not required by default 398 netconf = False # network configuration is not required by default
282 self.logger.debug("qemu started in %s seconds - qemu procces pid is %s (%s)" % 399 self.logger.debug("qemu started in %.2f seconds - qemu procces pid is %s (%s)" %
283 (time.time() - (endtime - self.runqemutime), 400 (time.time() - (endtime - self.runqemutime),
284 self.qemupid, time.strftime("%D %H:%M:%S"))) 401 self.qemupid, time.strftime("%D %H:%M:%S")))
285 cmdline = '' 402 cmdline = ''
@@ -291,9 +408,10 @@ class QemuRunner:
291 cmdline = re_control_char.sub(' ', cmdline) 408 cmdline = re_control_char.sub(' ', cmdline)
292 try: 409 try:
293 if self.use_slirp: 410 if self.use_slirp:
294 tcp_ports = cmdline.split("hostfwd=tcp::")[1] 411 tcp_ports = cmdline.split("hostfwd=tcp:")[1]
412 ip, tcp_ports = tcp_ports.split(":")[:2]
295 host_port = tcp_ports[:tcp_ports.find('-')] 413 host_port = tcp_ports[:tcp_ports.find('-')]
296 self.ip = "localhost:%s" % host_port 414 self.ip = "%s:%s" % (ip, host_port)
297 else: 415 else:
298 ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1]) 416 ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
299 self.ip = ips[0] 417 self.ip = ips[0]
@@ -301,8 +419,8 @@ class QemuRunner:
301 self.logger.debug("qemu cmdline used:\n{}".format(cmdline)) 419 self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
302 except (IndexError, ValueError): 420 except (IndexError, ValueError):
303 # Try to get network configuration from runqemu output 421 # Try to get network configuration from runqemu output
304 match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+)$.*', 422 match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+).*',
305 out, re.MULTILINE|re.DOTALL) 423 out, re.MULTILINE | re.DOTALL)
306 if match: 424 if match:
307 self.ip, self.server_ip, self.netmask = match.groups() 425 self.ip, self.server_ip, self.netmask = match.groups()
308 # network configuration is required as we couldn't get it 426 # network configuration is required as we couldn't get it
@@ -313,16 +431,16 @@ class QemuRunner:
313 self.logger.error("Couldn't get ip from qemu command line and runqemu output! " 431 self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
314 "Here is the qemu command line used:\n%s\n" 432 "Here is the qemu command line used:\n%s\n"
315 "and output from runqemu:\n%s" % (cmdline, out)) 433 "and output from runqemu:\n%s" % (cmdline, out))
316 self._dump_host()
317 self.stop() 434 self.stop()
318 return False 435 return False
319 436
320 self.logger.debug("Target IP: %s" % self.ip) 437 self.logger.debug("Target IP: %s" % self.ip)
321 self.logger.debug("Server IP: %s" % self.server_ip) 438 self.logger.debug("Server IP: %s" % self.server_ip)
322 439
440 self.thread = LoggingThread(self.log, self.threadsock, self.logger, self.runqemu.stdout)
441 self.thread.start()
442
323 if self.serial_ports >= 2: 443 if self.serial_ports >= 2:
324 self.thread = LoggingThread(self.log, self.threadsock, self.logger)
325 self.thread.start()
326 if not self.thread.connection_established.wait(self.boottime): 444 if not self.thread.connection_established.wait(self.boottime):
327 self.logger.error("Didn't receive a console connection from qemu. " 445 self.logger.error("Didn't receive a console connection from qemu. "
328 "Here is the qemu command line used:\n%s\nand " 446 "Here is the qemu command line used:\n%s\nand "
@@ -334,7 +452,7 @@ class QemuRunner:
334 self.logger.debug("Waiting at most %d seconds for login banner (%s)" % 452 self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
335 (self.boottime, time.strftime("%D %H:%M:%S"))) 453 (self.boottime, time.strftime("%D %H:%M:%S")))
336 endtime = time.time() + self.boottime 454 endtime = time.time() + self.boottime
337 socklist = [self.server_socket] 455 filelist = [self.server_socket]
338 reachedlogin = False 456 reachedlogin = False
339 stopread = False 457 stopread = False
340 qemusock = None 458 qemusock = None
@@ -342,64 +460,84 @@ class QemuRunner:
342 data = b'' 460 data = b''
343 while time.time() < endtime and not stopread: 461 while time.time() < endtime and not stopread:
344 try: 462 try:
345 sread, swrite, serror = select.select(socklist, [], [], 5) 463 sread, swrite, serror = select.select(filelist, [], [], 5)
346 except InterruptedError: 464 except InterruptedError:
347 continue 465 continue
348 for sock in sread: 466 for file in sread:
349 if sock is self.server_socket: 467 if file is self.server_socket:
350 qemusock, addr = self.server_socket.accept() 468 qemusock, addr = self.server_socket.accept()
351 qemusock.setblocking(0) 469 qemusock.setblocking(False)
352 socklist.append(qemusock) 470 filelist.append(qemusock)
353 socklist.remove(self.server_socket) 471 filelist.remove(self.server_socket)
354 self.logger.debug("Connection from %s:%s" % addr) 472 self.logger.debug("Connection from %s:%s" % addr)
355 else: 473 else:
356 data = data + sock.recv(1024) 474 # try to avoid reading only a single character at a time
475 time.sleep(0.1)
476 if hasattr(file, 'read'):
477 read = file.read(1024)
478 elif hasattr(file, 'recv'):
479 read = file.recv(1024)
480 else:
481 self.logger.error('Invalid file type: %s\n%s' % (file))
482 read = b''
483
484 self.logger.debug2('Partial boot log:\n%s' % (read.decode('utf-8', errors='backslashreplace')))
485 data = data + read
357 if data: 486 if data:
358 bootlog += data 487 bootlog += data
359 if self.serial_ports < 2: 488 self.log(data, extension = ".2")
360 # this socket has mixed console/kernel data, log it to logfile
361 self.log(data)
362
363 data = b'' 489 data = b''
364 if self.boot_patterns['search_reached_prompt'] in bootlog: 490
491 if bytes(self.boot_patterns['search_reached_prompt'], 'utf-8') in bootlog:
492 self.server_socket.close()
365 self.server_socket = qemusock 493 self.server_socket = qemusock
366 stopread = True 494 stopread = True
367 reachedlogin = True 495 reachedlogin = True
368 self.logger.debug("Reached login banner in %s seconds (%s)" % 496 self.logger.debug("Reached login banner in %.2f seconds (%s)" %
369 (time.time() - (endtime - self.boottime), 497 (time.time() - (endtime - self.boottime),
370 time.strftime("%D %H:%M:%S"))) 498 time.strftime("%D %H:%M:%S")))
371 else: 499 else:
372 # no need to check if reachedlogin unless we support multiple connections 500 # no need to check if reachedlogin unless we support multiple connections
373 self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" % 501 self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
374 time.strftime("%D %H:%M:%S")) 502 time.strftime("%D %H:%M:%S"))
375 socklist.remove(sock) 503 filelist.remove(file)
376 sock.close() 504 file.close()
377 stopread = True 505 stopread = True
378 506
379
380 if not reachedlogin: 507 if not reachedlogin:
381 if time.time() >= endtime: 508 if time.time() >= endtime:
382 self.logger.warning("Target didn't reach login banner in %d seconds (%s)" % 509 self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
383 (self.boottime, time.strftime("%D %H:%M:%S"))) 510 (self.boottime, time.strftime("%D %H:%M:%S")))
384 tail = lambda l: "\n".join(l.splitlines()[-25:]) 511 tail = lambda l: "\n".join(l.splitlines()[-25:])
385 bootlog = bootlog.decode("utf-8") 512 bootlog = self.decode_qemulog(bootlog)
386 # in case bootlog is empty, use tail qemu log store at self.msg 513 self.logger.warning("Last 25 lines of login console (%d):\n%s" % (len(bootlog), tail(bootlog)))
387 lines = tail(bootlog if bootlog else self.msg) 514 self.logger.warning("Last 25 lines of all logging (%d):\n%s" % (len(self.msg), tail(self.msg)))
388 self.logger.warning("Last 25 lines of text:\n%s" % lines)
389 self.logger.warning("Check full boot log: %s" % self.logfile) 515 self.logger.warning("Check full boot log: %s" % self.logfile)
390 self._dump_host()
391 self.stop() 516 self.stop()
517 data = True
518 while data:
519 try:
520 time.sleep(1)
521 data = qemusock.recv(1024)
522 self.log(data, extension = ".2")
523 self.logger.warning('Extra log data read: %s\n' % (data.decode('utf-8', errors='backslashreplace')))
524 except Exception as e:
525 self.logger.warning('Extra log data exception %s' % repr(e))
526 data = None
392 return False 527 return False
393 528
529 with self.thread.serial_lock:
530 self.thread.set_serialsock(self.server_socket)
531
394 # If we are not able to login the tests can continue 532 # If we are not able to login the tests can continue
395 try: 533 try:
396 (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120) 534 (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120)
397 if re.search(self.boot_patterns['search_login_succeeded'], output): 535 if re.search(self.boot_patterns['search_login_succeeded'], output):
398 self.logged = True 536 self.logged = True
399 self.logger.debug("Logged as root in serial console") 537 self.logger.debug("Logged in as %s in serial console" % self.boot_patterns['send_login_user'].replace("\n", ""))
400 if netconf: 538 if netconf:
401 # configure guest networking 539 # configure guest networking
402 cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask) 540 cmd = "ip addr add %s/%s dev eth0\nip link set dev eth0 up\n" % (self.ip, self.netmask)
403 output = self.run_serial(cmd, raw=True)[1] 541 output = self.run_serial(cmd, raw=True)[1]
404 if re.search(r"root@[a-zA-Z0-9\-]+:~#", output): 542 if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
405 self.logger.debug("configured ip address %s", self.ip) 543 self.logger.debug("configured ip address %s", self.ip)
@@ -407,7 +545,7 @@ class QemuRunner:
407 self.logger.debug("Couldn't configure guest networking") 545 self.logger.debug("Couldn't configure guest networking")
408 else: 546 else:
409 self.logger.warning("Couldn't login into serial console" 547 self.logger.warning("Couldn't login into serial console"
410 " as root using blank password") 548 " as %s using blank password" % self.boot_patterns['send_login_user'].replace("\n", ""))
411 self.logger.warning("The output:\n%s" % output) 549 self.logger.warning("The output:\n%s" % output)
412 except: 550 except:
413 self.logger.warning("Serial console failed while trying to login") 551 self.logger.warning("Serial console failed while trying to login")
@@ -427,16 +565,24 @@ class QemuRunner:
427 except OSError as e: 565 except OSError as e:
428 if e.errno != errno.ESRCH: 566 if e.errno != errno.ESRCH:
429 raise 567 raise
430 endtime = time.time() + self.runqemutime 568 try:
431 while self.runqemu.poll() is None and time.time() < endtime: 569 outs, errs = self.runqemu.communicate(timeout=self.runqemutime)
432 time.sleep(1) 570 if outs:
433 if self.runqemu.poll() is None: 571 self.logger.info("Output from runqemu:\n%s", outs.decode("utf-8"))
572 if errs:
573 self.logger.info("Stderr from runqemu:\n%s", errs.decode("utf-8"))
574 except subprocess.TimeoutExpired:
434 self.logger.debug("Sending SIGKILL to runqemu") 575 self.logger.debug("Sending SIGKILL to runqemu")
435 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL) 576 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
577 if not self.runqemu.stdout.closed:
578 self.logger.info("Output from runqemu:\n%s" % getOutput(self.runqemu.stdout))
436 self.runqemu.stdin.close() 579 self.runqemu.stdin.close()
437 self.runqemu.stdout.close() 580 self.runqemu.stdout.close()
438 self.runqemu_exited = True 581 self.runqemu_exited = True
439 582
583 if hasattr(self, 'qmp') and self.qmp:
584 self.qmp.close()
585 self.qmp = None
440 if hasattr(self, 'server_socket') and self.server_socket: 586 if hasattr(self, 'server_socket') and self.server_socket:
441 self.server_socket.close() 587 self.server_socket.close()
442 self.server_socket = None 588 self.server_socket = None
@@ -467,6 +613,11 @@ class QemuRunner:
467 self.thread.stop() 613 self.thread.stop()
468 self.thread.join() 614 self.thread.join()
469 615
616 def allowexit(self):
617 self.canexit = True
618 if self.thread:
619 self.thread.allowexit()
620
470 def restart(self, qemuparams = None): 621 def restart(self, qemuparams = None):
471 self.logger.warning("Restarting qemu process") 622 self.logger.warning("Restarting qemu process")
472 if self.runqemu.poll() is None: 623 if self.runqemu.poll() is None:
@@ -483,8 +634,12 @@ class QemuRunner:
483 # so it's possible that the file has been created but the content is empty 634 # so it's possible that the file has been created but the content is empty
484 pidfile_timeout = time.time() + 3 635 pidfile_timeout = time.time() + 3
485 while time.time() < pidfile_timeout: 636 while time.time() < pidfile_timeout:
486 with open(self.qemu_pidfile, 'r') as f: 637 try:
487 qemu_pid = f.read().strip() 638 with open(self.qemu_pidfile, 'r') as f:
639 qemu_pid = f.read().strip()
640 except FileNotFoundError:
641 # Can be used to detect shutdown so the pid file can disappear
642 return False
488 # file created but not yet written contents 643 # file created but not yet written contents
489 if not qemu_pid: 644 if not qemu_pid:
490 time.sleep(0.5) 645 time.sleep(0.5)
@@ -495,34 +650,49 @@ class QemuRunner:
495 return True 650 return True
496 return False 651 return False
497 652
653 def run_monitor(self, command, args=None, timeout=60):
654 if hasattr(self, 'qmp') and self.qmp:
655 self.qmp.settimeout(timeout)
656 if args is not None:
657 return self.qmp.cmd_raw(command, args)
658 else:
659 return self.qmp.cmd_raw(command)
660
498 def run_serial(self, command, raw=False, timeout=60): 661 def run_serial(self, command, raw=False, timeout=60):
662 # Returns (status, output) where status is 1 on success and 0 on error
663
499 # We assume target system have echo to get command status 664 # We assume target system have echo to get command status
500 if not raw: 665 if not raw:
501 command = "%s; echo $?\n" % command 666 command = "%s; echo $?\n" % command
502 667
503 data = '' 668 data = ''
504 status = 0 669 status = 0
505 self.server_socket.sendall(command.encode('utf-8')) 670 with self.thread.serial_lock:
506 start = time.time() 671 self.server_socket.sendall(command.encode('utf-8'))
507 end = start + timeout 672 start = time.time()
508 while True: 673 end = start + timeout
509 now = time.time() 674 while True:
510 if now >= end: 675 now = time.time()
511 data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout 676 if now >= end:
512 break 677 data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
513 try: 678 break
514 sread, _, _ = select.select([self.server_socket],[],[], end - now) 679 try:
515 except InterruptedError: 680 sread, _, _ = select.select([self.server_socket],[],[], end - now)
516 continue 681 except InterruptedError:
517 if sread: 682 continue
518 answer = self.server_socket.recv(1024) 683 if sread:
519 if answer: 684 # try to avoid reading single character at a time
520 data += answer.decode('utf-8') 685 time.sleep(0.1)
521 # Search the prompt to stop 686 answer = self.server_socket.recv(1024)
522 if re.search(self.boot_patterns['search_cmd_finished'], data): 687 if answer:
523 break 688 data += re_vt100.sub("", answer.decode('utf-8'))
524 else: 689 # Search the prompt to stop
525 raise Exception("No data on serial console socket") 690 if re.search(self.boot_patterns['search_cmd_finished'], data):
691 break
692 else:
693 if self.canexit:
694 return (1, "")
695 raise Exception("No data on serial console socket, connection closed?")
526 696
527 if data: 697 if data:
528 if raw: 698 if raw:
@@ -541,34 +711,48 @@ class QemuRunner:
541 status = 1 711 status = 1
542 return (status, str(data)) 712 return (status, str(data))
543 713
544 714@contextmanager
545 def _dump_host(self): 715def nonblocking_lock(lock):
546 self.host_dumper.create_dir("qemu") 716 locked = lock.acquire(False)
547 self.logger.warning("Qemu ended unexpectedly, dump data from host" 717 try:
548 " is in %s" % self.host_dumper.dump_dir) 718 yield locked
549 self.host_dumper.dump_host() 719 finally:
720 if locked:
721 lock.release()
550 722
551# This class is for reading data from a socket and passing it to logfunc 723# This class is for reading data from a socket and passing it to logfunc
552# to be processed. It's completely event driven and has a straightforward 724# to be processed. It's completely event driven and has a straightforward
553# event loop. The mechanism for stopping the thread is a simple pipe which 725# event loop. The mechanism for stopping the thread is a simple pipe which
554# will wake up the poll and allow for tearing everything down. 726# will wake up the poll and allow for tearing everything down.
555class LoggingThread(threading.Thread): 727class LoggingThread(threading.Thread):
556 def __init__(self, logfunc, sock, logger): 728 def __init__(self, logfunc, sock, logger, qemuoutput):
557 self.connection_established = threading.Event() 729 self.connection_established = threading.Event()
730 self.serial_lock = threading.Lock()
731
558 self.serversock = sock 732 self.serversock = sock
733 self.serialsock = None
734 self.qemuoutput = qemuoutput
559 self.logfunc = logfunc 735 self.logfunc = logfunc
560 self.logger = logger 736 self.logger = logger
561 self.readsock = None 737 self.readsock = None
562 self.running = False 738 self.running = False
739 self.canexit = False
563 740
564 self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL 741 self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
565 self.readevents = select.POLLIN | select.POLLPRI 742 self.readevents = select.POLLIN | select.POLLPRI
566 743
567 threading.Thread.__init__(self, target=self.threadtarget) 744 threading.Thread.__init__(self, target=self.threadtarget)
568 745
746 def set_serialsock(self, serialsock):
747 self.serialsock = serialsock
748
569 def threadtarget(self): 749 def threadtarget(self):
570 try: 750 try:
571 self.eventloop() 751 self.eventloop()
752 except Exception:
753 exc_type, exc_value, exc_traceback = sys.exc_info()
754 self.logger.warning("Exception %s in logging thread" %
755 traceback.format_exception(exc_type, exc_value, exc_traceback))
572 finally: 756 finally:
573 self.teardown() 757 self.teardown()
574 758
@@ -584,7 +768,8 @@ class LoggingThread(threading.Thread):
584 768
585 def teardown(self): 769 def teardown(self):
586 self.logger.debug("Tearing down logging thread") 770 self.logger.debug("Tearing down logging thread")
587 self.close_socket(self.serversock) 771 if self.serversock:
772 self.close_socket(self.serversock)
588 773
589 if self.readsock is not None: 774 if self.readsock is not None:
590 self.close_socket(self.readsock) 775 self.close_socket(self.readsock)
@@ -593,30 +778,37 @@ class LoggingThread(threading.Thread):
593 self.close_ignore_error(self.writepipe) 778 self.close_ignore_error(self.writepipe)
594 self.running = False 779 self.running = False
595 780
781 def allowexit(self):
782 self.canexit = True
783
596 def eventloop(self): 784 def eventloop(self):
597 poll = select.poll() 785 poll = select.poll()
598 event_read_mask = self.errorevents | self.readevents 786 event_read_mask = self.errorevents | self.readevents
599 poll.register(self.serversock.fileno()) 787 if self.serversock:
788 poll.register(self.serversock.fileno())
789 serial_registered = False
790 poll.register(self.qemuoutput.fileno())
600 poll.register(self.readpipe, event_read_mask) 791 poll.register(self.readpipe, event_read_mask)
601 792
602 breakout = False 793 breakout = False
603 self.running = True 794 self.running = True
604 self.logger.debug("Starting thread event loop") 795 self.logger.debug("Starting thread event loop")
605 while not breakout: 796 while not breakout:
606 events = poll.poll() 797 events = poll.poll(2)
607 for event in events: 798 for fd, event in events:
799
608 # An error occurred, bail out 800 # An error occurred, bail out
609 if event[1] & self.errorevents: 801 if event & self.errorevents:
610 raise Exception(self.stringify_event(event[1])) 802 raise Exception(self.stringify_event(event))
611 803
612 # Event to stop the thread 804 # Event to stop the thread
613 if self.readpipe == event[0]: 805 if self.readpipe == fd:
614 self.logger.debug("Stop event received") 806 self.logger.debug("Stop event received")
615 breakout = True 807 breakout = True
616 break 808 break
617 809
618 # A connection request was received 810 # A connection request was received
619 elif self.serversock.fileno() == event[0]: 811 elif self.serversock and self.serversock.fileno() == fd:
620 self.logger.debug("Connection request received") 812 self.logger.debug("Connection request received")
621 self.readsock, _ = self.serversock.accept() 813 self.readsock, _ = self.serversock.accept()
622 self.readsock.setblocking(0) 814 self.readsock.setblocking(0)
@@ -627,18 +819,40 @@ class LoggingThread(threading.Thread):
627 self.connection_established.set() 819 self.connection_established.set()
628 820
629 # Actual data to be logged 821 # Actual data to be logged
630 elif self.readsock.fileno() == event[0]: 822 elif self.readsock and self.readsock.fileno() == fd:
631 data = self.recv(1024) 823 data = self.recv(1024, self.readsock)
632 self.logfunc(data) 824 self.logfunc(data)
825 elif self.qemuoutput.fileno() == fd:
826 data = self.qemuoutput.read()
827 self.logger.debug("Data received on qemu stdout %s" % data)
828 self.logfunc(data, ".stdout")
829 elif self.serialsock and self.serialsock.fileno() == fd:
830 if self.serial_lock.acquire(blocking=False):
831 try:
832 data = self.recv(1024, self.serialsock)
833 self.logger.debug("Data received serial thread %s" % data.decode('utf-8', 'replace'))
834 self.logfunc(data, ".2")
835 finally:
836 self.serial_lock.release()
837 else:
838 serial_registered = False
839 poll.unregister(self.serialsock.fileno())
840
841 if not serial_registered and self.serialsock:
842 with nonblocking_lock(self.serial_lock) as l:
843 if l:
844 serial_registered = True
845 poll.register(self.serialsock.fileno(), event_read_mask)
846
633 847
634 # Since the socket is non-blocking make sure to honor EAGAIN 848 # Since the socket is non-blocking make sure to honor EAGAIN
635 # and EWOULDBLOCK. 849 # and EWOULDBLOCK.
636 def recv(self, count): 850 def recv(self, count, sock):
637 try: 851 try:
638 data = self.readsock.recv(count) 852 data = sock.recv(count)
639 except socket.error as e: 853 except socket.error as e:
640 if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK: 854 if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
641 return '' 855 return b''
642 else: 856 else:
643 raise 857 raise
644 858
@@ -649,7 +863,9 @@ class LoggingThread(threading.Thread):
649 # happened. But for this code it counts as an 863 # happened. But for this code it counts as an
650 # error since the connection shouldn't go away 864 # error since the connection shouldn't go away
651 # until qemu exits. 865 # until qemu exits.
652 raise Exception("Console connection closed unexpectedly") 866 if not self.canexit:
867 raise Exception("Console connection closed unexpectedly")
868 return b''
653 869
654 return data 870 return data
655 871
@@ -661,6 +877,9 @@ class LoggingThread(threading.Thread):
661 val = 'POLLHUP' 877 val = 'POLLHUP'
662 elif select.POLLNVAL == event: 878 elif select.POLLNVAL == event:
663 val = 'POLLNVAL' 879 val = 'POLLNVAL'
880 else:
881 val = "0x%x" % (event)
882
664 return val 883 return val
665 884
666 def close_socket(self, sock): 885 def close_socket(self, sock):
diff --git a/meta/lib/oeqa/utils/qemutinyrunner.py b/meta/lib/oeqa/utils/qemutinyrunner.py
index 5c92941c0a..20009401ca 100644
--- a/meta/lib/oeqa/utils/qemutinyrunner.py
+++ b/meta/lib/oeqa/utils/qemutinyrunner.py
@@ -19,7 +19,7 @@ from .qemurunner import QemuRunner
19 19
20class QemuTinyRunner(QemuRunner): 20class QemuTinyRunner(QemuRunner):
21 21
22 def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, kernel, boottime, logger): 22 def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, kernel, boottime, logger, tmpfsdir=None):
23 23
24 # Popen object for runqemu 24 # Popen object for runqemu
25 self.runqemu = None 25 self.runqemu = None
@@ -37,6 +37,7 @@ class QemuTinyRunner(QemuRunner):
37 self.deploy_dir_image = deploy_dir_image 37 self.deploy_dir_image = deploy_dir_image
38 self.logfile = logfile 38 self.logfile = logfile
39 self.boottime = boottime 39 self.boottime = boottime
40 self.tmpfsdir = tmpfsdir
40 41
41 self.runqemutime = 60 42 self.runqemutime = 60
42 self.socketfile = "console.sock" 43 self.socketfile = "console.sock"
@@ -83,6 +84,9 @@ class QemuTinyRunner(QemuRunner):
83 return False 84 return False
84 else: 85 else:
85 os.environ["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image 86 os.environ["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
87 if self.tmpfsdir:
88 env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
89
86 90
87 # Set this flag so that Qemu doesn't do any grabs as SDL grabs interact 91 # Set this flag so that Qemu doesn't do any grabs as SDL grabs interact
88 # badly with screensavers. 92 # badly with screensavers.
diff --git a/meta/lib/oeqa/utils/sshcontrol.py b/meta/lib/oeqa/utils/sshcontrol.py
index 36c2ecb3db..88a61aff63 100644
--- a/meta/lib/oeqa/utils/sshcontrol.py
+++ b/meta/lib/oeqa/utils/sshcontrol.py
@@ -57,8 +57,10 @@ class SSHProcess(object):
57 if select.select([self.process.stdout], [], [], 5)[0] != []: 57 if select.select([self.process.stdout], [], [], 5)[0] != []:
58 data = os.read(self.process.stdout.fileno(), 1024) 58 data = os.read(self.process.stdout.fileno(), 1024)
59 if not data: 59 if not data:
60 self.process.stdout.close() 60 self.process.poll()
61 eof = True 61 if self.process.returncode is not None:
62 self.process.stdout.close()
63 eof = True
62 else: 64 else:
63 data = data.decode("utf-8") 65 data = data.decode("utf-8")
64 output += data 66 output += data
diff --git a/meta/lib/oeqa/utils/subprocesstweak.py b/meta/lib/oeqa/utils/subprocesstweak.py
index b47975a4bc..1774513023 100644
--- a/meta/lib/oeqa/utils/subprocesstweak.py
+++ b/meta/lib/oeqa/utils/subprocesstweak.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: MIT 4# SPDX-License-Identifier: MIT
3# 5#
4import subprocess 6import subprocess
@@ -6,16 +8,11 @@ import subprocess
6class OETestCalledProcessError(subprocess.CalledProcessError): 8class OETestCalledProcessError(subprocess.CalledProcessError):
7 def __str__(self): 9 def __str__(self):
8 def strify(o): 10 def strify(o):
9 if isinstance(o, bytes): 11 return o.decode("utf-8", errors="replace") if isinstance(o, bytes) else o
10 return o.decode("utf-8", errors="replace")
11 else:
12 return o
13 12
14 s = "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) 13 s = super().__str__()
15 if hasattr(self, "output") and self.output: 14 s = s + "\nStandard Output: " + strify(self.output)
16 s = s + "\nStandard Output: " + strify(self.output) 15 s = s + "\nStandard Error: " + strify(self.stderr)
17 if hasattr(self, "stderr") and self.stderr:
18 s = s + "\nStandard Error: " + strify(self.stderr)
19 return s 16 return s
20 17
21def errors_have_output(): 18def errors_have_output():
diff --git a/meta/lib/oeqa/utils/targetbuild.py b/meta/lib/oeqa/utils/targetbuild.py
index 1055810ca3..09738add1d 100644
--- a/meta/lib/oeqa/utils/targetbuild.py
+++ b/meta/lib/oeqa/utils/targetbuild.py
@@ -19,6 +19,7 @@ class BuildProject(metaclass=ABCMeta):
19 self.d = d 19 self.d = d
20 self.uri = uri 20 self.uri = uri
21 self.archive = os.path.basename(uri) 21 self.archive = os.path.basename(uri)
22 self.tempdirobj = None
22 if not tmpdir: 23 if not tmpdir:
23 tmpdir = self.d.getVar('WORKDIR') 24 tmpdir = self.d.getVar('WORKDIR')
24 if not tmpdir: 25 if not tmpdir:
@@ -71,9 +72,10 @@ class BuildProject(metaclass=ABCMeta):
71 return self._run('cd %s; make install %s' % (self.targetdir, install_args)) 72 return self._run('cd %s; make install %s' % (self.targetdir, install_args))
72 73
73 def clean(self): 74 def clean(self):
75 if self.tempdirobj:
76 self.tempdirobj.cleanup()
74 self._run('rm -rf %s' % self.targetdir) 77 self._run('rm -rf %s' % self.targetdir)
75 subprocess.check_call('rm -f %s' % self.localarchive, shell=True) 78 subprocess.check_call('rm -f %s' % self.localarchive, shell=True)
76 pass
77 79
78class TargetBuildProject(BuildProject): 80class TargetBuildProject(BuildProject):
79 81
diff --git a/meta/lib/oeqa/utils/testexport.py b/meta/lib/oeqa/utils/testexport.py
index e89d130a9c..3ab024d9e9 100644
--- a/meta/lib/oeqa/utils/testexport.py
+++ b/meta/lib/oeqa/utils/testexport.py
@@ -60,17 +60,17 @@ def process_binaries(d, params):
60 export_env = d.getVar("TEST_EXPORT_ONLY") 60 export_env = d.getVar("TEST_EXPORT_ONLY")
61 61
62 def extract_binary(pth_to_pkg, dest_pth=None): 62 def extract_binary(pth_to_pkg, dest_pth=None):
63 cpio_command = runCmd("which cpio") 63 tar_command = runCmd("which tar")
64 rpm2cpio_command = runCmd("ls /usr/bin/rpm2cpio") 64 rpm2archive_command = runCmd("ls /usr/bin/rpm2archive")
65 if (cpio_command.status != 0) and (rpm2cpio_command.status != 0): 65 if (tar_command.status != 0) and (rpm2archive_command.status != 0):
66 bb.fatal("Either \"rpm2cpio\" or \"cpio\" tools are not available on your system." 66 bb.fatal("Either \"rpm2archive\" or \"tar\" tools are not available on your system."
67 "All binaries extraction processes will not be available, crashing all related tests." 67 "All binaries extraction processes will not be available, crashing all related tests."
68 "Please install them according to your OS recommendations") # will exit here 68 "Please install them according to your OS recommendations") # will exit here
69 if dest_pth: 69 if dest_pth:
70 os.chdir(dest_pth) 70 os.chdir(dest_pth)
71 else: 71 else:
72 os.chdir("%s" % os.sep)# this is for native package 72 os.chdir("%s" % os.sep)# this is for native package
73 extract_bin_command = runCmd("%s %s | %s -idm" % (rpm2cpio_command.output, pth_to_pkg, cpio_command.output)) # semi-hardcoded because of a bug on poky's rpm2cpio 73 extract_bin_command = runCmd("%s -n %s | %s xv" % (rpm2archive_command.output, pth_to_pkg, tar_command.output)) # semi-hardcoded because of a bug on poky's rpm2cpio
74 return extract_bin_command 74 return extract_bin_command
75 75
76 if determine_if_poky_env(): # machine with poky environment 76 if determine_if_poky_env(): # machine with poky environment