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__.py9
-rw-r--r--meta/lib/oeqa/utils/buildproject.py3
-rw-r--r--meta/lib/oeqa/utils/commands.py39
-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.py56
-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.py6
-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.py98
-rw-r--r--meta/lib/oeqa/utils/qemurunner.py447
-rw-r--r--meta/lib/oeqa/utils/qemutinyrunner.py6
-rw-r--r--meta/lib/oeqa/utils/subprocesstweak.py2
-rw-r--r--meta/lib/oeqa/utils/targetbuild.py4
18 files changed, 687 insertions, 300 deletions
diff --git a/meta/lib/oeqa/utils/__init__.py b/meta/lib/oeqa/utils/__init__.py
index 6d1ec4cb99..53bdcbf266 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,10 @@ 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 \ No newline at end of file
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..575e380017 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()
@@ -283,8 +285,10 @@ def get_bb_vars(variables=None, target=None, postconfig=None):
283def get_bb_var(var, target=None, postconfig=None): 285def get_bb_var(var, target=None, postconfig=None):
284 return get_bb_vars([var], target, postconfig)[var] 286 return get_bb_vars([var], target, postconfig)[var]
285 287
286def get_test_layer(): 288def get_test_layer(bblayers=None):
287 layers = get_bb_var("BBLAYERS").split() 289 if bblayers is None:
290 bblayers = get_bb_var("BBLAYERS")
291 layers = bblayers.split()
288 testlayer = None 292 testlayer = None
289 for l in layers: 293 for l in layers:
290 if '~' in l: 294 if '~' in l:
@@ -296,6 +300,7 @@ def get_test_layer():
296 300
297def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'): 301def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
298 os.makedirs(os.path.join(templayerdir, 'conf')) 302 os.makedirs(os.path.join(templayerdir, 'conf'))
303 corenames = get_bb_var('LAYERSERIES_CORENAMES')
299 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f: 304 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
300 f.write('BBPATH .= ":${LAYERDIR}"\n') 305 f.write('BBPATH .= ":${LAYERDIR}"\n')
301 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec) 306 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
@@ -304,7 +309,7 @@ def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec=
304 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername) 309 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
305 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority)) 310 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
306 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername) 311 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
307 f.write('LAYERSERIES_COMPAT_%s = "${LAYERSERIES_COMPAT_core}"\n' % templayername) 312 f.write('LAYERSERIES_COMPAT_%s = "%s"\n' % (templayername, corenames))
308 313
309@contextlib.contextmanager 314@contextlib.contextmanager
310def runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None, qemuparams=None, overrides={}, discard_writes=True): 315def runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None, qemuparams=None, overrides={}, discard_writes=True):
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..10cb267dfa 100644
--- a/meta/lib/oeqa/utils/gitarchive.py
+++ b/meta/lib/oeqa/utils/gitarchive.py
@@ -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
@@ -116,7 +151,7 @@ def expand_tag_strings(repo, name_pattern, msg_subj_pattern, msg_body_pattern,
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,
@@ -181,7 +217,7 @@ def get_test_runs(log, repo, tag_name, **kwargs):
181 217
182 # Get a list of all matching tags 218 # Get a list of all matching tags
183 tag_pattern = tag_name.format(**str_fields) 219 tag_pattern = tag_name.format(**str_fields)
184 tags = repo.run_cmd(['tag', '-l', tag_pattern]).splitlines() 220 tags = get_tags(repo, log, pattern=tag_pattern)
185 log.debug("Found %d tags matching pattern '%s'", len(tags), tag_pattern) 221 log.debug("Found %d tags matching pattern '%s'", len(tags), tag_pattern)
186 222
187 # Parse undefined fields from tag names 223 # Parse undefined fields from tag names
@@ -199,6 +235,8 @@ def get_test_runs(log, repo, tag_name, **kwargs):
199 revs = [] 235 revs = []
200 for tag in tags: 236 for tag in tags:
201 m = tag_re.match(tag) 237 m = tag_re.match(tag)
238 if not m:
239 continue
202 groups = m.groupdict() 240 groups = m.groupdict()
203 revs.append([groups[f] for f in undef_fields] + [tag]) 241 revs.append([groups[f] for f in undef_fields] + [tag])
204 242
@@ -219,7 +257,15 @@ def get_test_revs(log, repo, tag_name, **kwargs):
219 if not commit in revs: 257 if not commit in revs:
220 revs[commit] = TestedRev(commit, commit_num, [tag]) 258 revs[commit] = TestedRev(commit, commit_num, [tag])
221 else: 259 else:
222 assert commit_num == revs[commit].commit_number, "Commit numbers do not match" 260 if commit_num != revs[commit].commit_number:
261 # Historically we have incorrect commit counts of '1' in the repo so fix these up
262 if int(revs[commit].commit_number) < 5:
263 tags = revs[commit].tags
264 revs[commit] = TestedRev(commit, commit_num, [tags])
265 elif int(commit_num) < 5:
266 pass
267 else:
268 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) 269 revs[commit].tags.append(tag)
224 270
225 # Return in sorted table 271 # 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..15ec190c4a 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()
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..ecdddd2d40
--- /dev/null
+++ b/meta/lib/oeqa/utils/postactions.py
@@ -0,0 +1,98 @@
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
10from oeqa.utils import get_json_result_dir
11
12def create_artifacts_directory(d, tc):
13 import shutil
14
15 local_artifacts_dir = os.path.join(get_json_result_dir(d), "artifacts")
16 if os.path.isdir(local_artifacts_dir):
17 shutil.rmtree(local_artifacts_dir)
18
19 os.makedirs(local_artifacts_dir)
20
21##################################################################
22# Host/target statistics
23##################################################################
24
25def get_target_disk_usage(d, tc):
26 output_file = os.path.join(get_json_result_dir(d), "artifacts", "target_disk_usage.txt")
27 try:
28 (status, output) = tc.target.run('df -h')
29 with open(output_file, 'w') as f:
30 f.write(output)
31 f.write("\n")
32 except Exception as e:
33 bb.warn(f"Can not get target disk usage: {e}")
34
35def get_host_disk_usage(d, tc):
36 import subprocess
37
38 output_file = os.path.join(get_json_result_dir(d), "artifacts", "host_disk_usage.txt")
39 try:
40 with open(output_file, 'w') as f:
41 output = subprocess.run(['df', '-hl'], check=True, text=True, stdout=f, env={})
42 except Exception as e:
43 bb.warn(f"Can not get host disk usage: {e}")
44
45##################################################################
46# Artifacts retrieval
47##################################################################
48
49def get_artifacts_list(target, raw_list):
50 result = []
51 # Passed list may contains patterns in paths, expand them directly on target
52 for raw_path in raw_list.split():
53 cmd = f"for p in {raw_path}; do if [ -e $p ]; then echo $p; fi; done"
54 try:
55 status, output = target.run(cmd)
56 if status != 0 or not output:
57 raise Exception()
58 result += output.split()
59 except:
60 bb.note(f"No file/directory matching path {raw_path}")
61
62 return result
63
64def retrieve_test_artifacts(target, artifacts_list, target_dir):
65 local_artifacts_dir = os.path.join(target_dir, "artifacts")
66 for artifact_path in artifacts_list:
67 if not os.path.isabs(artifact_path):
68 bb.warn(f"{artifact_path} is not an absolute path")
69 continue
70 try:
71 dest_dir = os.path.join(local_artifacts_dir, os.path.dirname(artifact_path[1:]))
72 os.makedirs(dest_dir, exist_ok=True)
73 target.copyFrom(artifact_path, dest_dir)
74 except Exception as e:
75 bb.warn(f"Can not retrieve {artifact_path} from test target: {e}")
76
77def list_and_fetch_failed_tests_artifacts(d, tc):
78 artifacts_list = get_artifacts_list(tc.target, d.getVar("TESTIMAGE_FAILED_QA_ARTIFACTS"))
79 if not artifacts_list:
80 bb.warn("Could not load artifacts list, skip artifacts retrieval")
81 else:
82 retrieve_test_artifacts(tc.target, artifacts_list, get_json_result_dir(d))
83
84
85##################################################################
86# General post actions runner
87##################################################################
88
89def run_failed_tests_post_actions(d, tc):
90 post_actions=[
91 create_artifacts_directory,
92 list_and_fetch_failed_tests_artifacts,
93 get_target_disk_usage,
94 get_host_disk_usage
95 ]
96
97 for action in post_actions:
98 action(d, tc)
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py
index 77ec939ad7..cda43aad8c 100644
--- a/meta/lib/oeqa/utils/qemurunner.py
+++ b/meta/lib/oeqa/utils/qemurunner.py
@@ -19,9 +19,11 @@ 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))
@@ -29,10 +31,19 @@ control_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)))
31 33
34def getOutput(o):
35 import fcntl
36 fl = fcntl.fcntl(o, fcntl.F_GETFL)
37 fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
38 try:
39 return os.read(o.fileno(), 1000000).decode("utf-8")
40 except BlockingIOError:
41 return ""
42
32class QemuRunner: 43class QemuRunner:
33 44
34 def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds, 45 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): 46 serial_ports=2, boot_patterns = defaultdict(str), use_ovmf=False, workdir=None, tmpfsdir=None):
36 47
37 # Popen object for runqemu 48 # Popen object for runqemu
38 self.runqemu = None 49 self.runqemu = None
@@ -55,21 +66,24 @@ class QemuRunner:
55 self.boottime = boottime 66 self.boottime = boottime
56 self.logged = False 67 self.logged = False
57 self.thread = None 68 self.thread = None
69 self.threadsock = None
58 self.use_kvm = use_kvm 70 self.use_kvm = use_kvm
59 self.use_ovmf = use_ovmf 71 self.use_ovmf = use_ovmf
60 self.use_slirp = use_slirp 72 self.use_slirp = use_slirp
61 self.serial_ports = serial_ports 73 self.serial_ports = serial_ports
62 self.msg = '' 74 self.msg = ''
63 self.boot_patterns = boot_patterns 75 self.boot_patterns = boot_patterns
76 self.tmpfsdir = tmpfsdir
64 77
65 self.runqemutime = 120 78 self.runqemutime = 300
66 if not workdir: 79 if not workdir:
67 workdir = os.getcwd() 80 workdir = os.getcwd()
68 self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid()) 81 self.qemu_pidfile = workdir + '/pidfile_' + str(os.getpid())
69 self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
70 self.monitorpipe = None 82 self.monitorpipe = None
71 83
72 self.logger = logger 84 self.logger = logger
85 # Whether we're expecting an exit and should show related errors
86 self.canexit = False
73 87
74 # Enable testing other OS's 88 # Enable testing other OS's
75 # Set commands for target communication, and default to Linux ALWAYS 89 # Set commands for target communication, and default to Linux ALWAYS
@@ -80,7 +94,7 @@ class QemuRunner:
80 accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished'] 94 accepted_patterns = ['search_reached_prompt', 'send_login_user', 'search_login_succeeded', 'search_cmd_finished']
81 default_boot_patterns = defaultdict(str) 95 default_boot_patterns = defaultdict(str)
82 # Default to the usual paterns used to communicate with the target 96 # Default to the usual paterns used to communicate with the target
83 default_boot_patterns['search_reached_prompt'] = b' login:' 97 default_boot_patterns['search_reached_prompt'] = ' login:'
84 default_boot_patterns['send_login_user'] = 'root\n' 98 default_boot_patterns['send_login_user'] = 'root\n'
85 default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#" 99 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\-]+:~#" 100 default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
@@ -94,6 +108,7 @@ class QemuRunner:
94 try: 108 try:
95 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 109 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
96 sock.setblocking(0) 110 sock.setblocking(0)
111 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
97 sock.bind(("127.0.0.1",0)) 112 sock.bind(("127.0.0.1",0))
98 sock.listen(2) 113 sock.listen(2)
99 port = sock.getsockname()[1] 114 port = sock.getsockname()[1]
@@ -104,30 +119,24 @@ class QemuRunner:
104 sock.close() 119 sock.close()
105 raise 120 raise
106 121
107 def log(self, msg): 122 def decode_qemulog(self, todecode):
108 if self.logfile: 123 # Sanitize the data received from qemu as it may contain control characters
109 # It is needed to sanitize the data received from qemu 124 msg = todecode.decode("utf-8", errors='backslashreplace')
110 # because is possible to have control characters 125 msg = re_control_char.sub('', msg)
111 msg = msg.decode("utf-8", errors='ignore') 126 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 127
128 def log(self, msg, extension=""):
129 if self.logfile:
130 with codecs.open(self.logfile + extension, "ab") as f:
131 f.write(msg)
132 self.msg += self.decode_qemulog(msg)
123 133
124 def handleSIGCHLD(self, signum, frame): 134 def handleSIGCHLD(self, signum, frame):
125 if self.runqemu and self.runqemu.poll(): 135 if self.runqemu and self.runqemu.poll():
126 if self.runqemu.returncode: 136 if self.runqemu.returncode:
127 self.logger.error('runqemu exited with code %d' % self.runqemu.returncode) 137 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)) 138 self.logger.error('Output from runqemu:\n%s' % getOutput(self.runqemu.stdout))
129 self.stop() 139 self.stop()
130 self._dump_host()
131 140
132 def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True): 141 def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
133 env = os.environ.copy() 142 env = os.environ.copy()
@@ -150,6 +159,9 @@ class QemuRunner:
150 else: 159 else:
151 env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image 160 env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
152 161
162 if self.tmpfsdir:
163 env["RUNQEMU_TMPFS_DIR"] = self.tmpfsdir
164
153 if not launch_cmd: 165 if not launch_cmd:
154 launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '') 166 launch_cmd = 'runqemu %s' % ('snapshot' if discard_writes else '')
155 if self.use_kvm: 167 if self.use_kvm:
@@ -163,11 +175,38 @@ class QemuRunner:
163 launch_cmd += ' slirp' 175 launch_cmd += ' slirp'
164 if self.use_ovmf: 176 if self.use_ovmf:
165 launch_cmd += ' ovmf' 177 launch_cmd += ' ovmf'
166 launch_cmd += ' %s %s %s' % (runqemuparams, self.machine, self.rootfs) 178 launch_cmd += ' %s %s' % (runqemuparams, self.machine)
179 if self.rootfs.endswith('.vmdk'):
180 self.logger.debug('Bypassing VMDK rootfs for runqemu')
181 else:
182 launch_cmd += ' %s' % (self.rootfs)
167 183
168 return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env) 184 return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
169 185
170 def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None): 186 def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
187 # use logfile to determine the recipe-sysroot-native path and
188 # then add in the site-packages path components and add that
189 # to the python sys.path so the qmp module can be found.
190 python_path = os.path.dirname(os.path.dirname(self.logfile))
191 python_path += "/recipe-sysroot-native/usr/lib/qemu-python"
192 sys.path.append(python_path)
193 importlib.invalidate_caches()
194 try:
195 qmp = importlib.import_module("qmp")
196 except Exception as e:
197 self.logger.error("qemurunner: qmp module missing, please ensure it's installed in %s (%s)" % (python_path, str(e)))
198 return False
199 # Path relative to tmpdir used as cwd for qemu below to avoid unix socket path length issues
200 qmp_file = "." + next(tempfile._get_candidate_names())
201 qmp_param = ' -S -qmp unix:./%s,server,wait' % (qmp_file)
202 qmp_port = self.tmpdir + "/" + qmp_file
203 # Create a second socket connection for debugging use,
204 # note this will NOT cause qemu to block waiting for the connection
205 qmp_file2 = "." + next(tempfile._get_candidate_names())
206 qmp_param += ' -qmp unix:./%s,server,nowait' % (qmp_file2)
207 qmp_port2 = self.tmpdir + "/" + qmp_file2
208 self.logger.info("QMP Available for connection at %s" % (qmp_port2))
209
171 try: 210 try:
172 if self.serial_ports >= 2: 211 if self.serial_ports >= 2:
173 self.threadsock, threadport = self.create_socket() 212 self.threadsock, threadport = self.create_socket()
@@ -176,7 +215,7 @@ class QemuRunner:
176 self.logger.error("Failed to create listening socket: %s" % msg[1]) 215 self.logger.error("Failed to create listening socket: %s" % msg[1])
177 return False 216 return False
178 217
179 bootparams = 'console=tty1 console=ttyS0,115200n8 printk.time=1' 218 bootparams = ' printk.time=1'
180 if extra_bootparams: 219 if extra_bootparams:
181 bootparams = bootparams + ' ' + extra_bootparams 220 bootparams = bootparams + ' ' + extra_bootparams
182 221
@@ -184,7 +223,8 @@ class QemuRunner:
184 # and analyze descendents in order to determine it. 223 # and analyze descendents in order to determine it.
185 if os.path.exists(self.qemu_pidfile): 224 if os.path.exists(self.qemu_pidfile):
186 os.remove(self.qemu_pidfile) 225 os.remove(self.qemu_pidfile)
187 self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile) 226 self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
227
188 if qemuparams: 228 if qemuparams:
189 self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"' 229 self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
190 230
@@ -196,14 +236,15 @@ class QemuRunner:
196 self.origchldhandler = signal.getsignal(signal.SIGCHLD) 236 self.origchldhandler = signal.getsignal(signal.SIGCHLD)
197 signal.signal(signal.SIGCHLD, self.handleSIGCHLD) 237 signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
198 238
199 self.logger.debug('launchcmd=%s'%(launch_cmd)) 239 self.logger.debug('launchcmd=%s' % (launch_cmd))
200 240
201 # FIXME: We pass in stdin=subprocess.PIPE here to work around stty 241 # 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 242 # blocking at the end of the runqemu script when using this within
203 # oe-selftest (this makes stty error out immediately). There ought 243 # oe-selftest (this makes stty error out immediately). There ought
204 # to be a proper fix but this will suffice for now. 244 # 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) 245 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 246 output = self.runqemu.stdout
247 launch_time = time.time()
207 248
208 # 249 #
209 # We need the preexec_fn above so that all runqemu processes can easily be killed 250 # We need the preexec_fn above so that all runqemu processes can easily be killed
@@ -229,30 +270,33 @@ class QemuRunner:
229 r = os.fdopen(r) 270 r = os.fdopen(r)
230 x = r.read() 271 x = r.read()
231 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM) 272 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
232 sys.exit(0) 273 os._exit(0)
233 274
234 self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid) 275 self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
235 self.logger.debug("waiting at most %s seconds for qemu pid (%s)" % 276 self.logger.debug("waiting at most %d seconds for qemu pid (%s)" %
236 (self.runqemutime, time.strftime("%D %H:%M:%S"))) 277 (self.runqemutime, time.strftime("%D %H:%M:%S")))
237 endtime = time.time() + self.runqemutime 278 endtime = time.time() + self.runqemutime
238 while not self.is_alive() and time.time() < endtime: 279 while not self.is_alive() and time.time() < endtime:
239 if self.runqemu.poll(): 280 if self.runqemu.poll():
240 if self.runqemu_exited: 281 if self.runqemu_exited:
282 self.logger.warning("runqemu during is_alive() test")
241 return False 283 return False
242 if self.runqemu.returncode: 284 if self.runqemu.returncode:
243 # No point waiting any longer 285 # No point waiting any longer
244 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode) 286 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
245 self._dump_host() 287 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() 288 self.stop()
248 return False 289 return False
249 time.sleep(0.5) 290 time.sleep(0.5)
250 291
251 if self.runqemu_exited: 292 if self.runqemu_exited:
252 return False 293 self.logger.warning("runqemu after timeout")
294
295 if self.runqemu.returncode:
296 self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
253 297
254 if not self.is_alive(): 298 if not self.is_alive():
255 self.logger.error("Qemu pid didn't appear in %s seconds (%s)" % 299 self.logger.error("Qemu pid didn't appear in %d seconds (%s)" %
256 (self.runqemutime, time.strftime("%D %H:%M:%S"))) 300 (self.runqemutime, time.strftime("%D %H:%M:%S")))
257 301
258 qemu_pid = None 302 qemu_pid = None
@@ -267,8 +311,7 @@ class QemuRunner:
267 ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0] 311 ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,pri,ni,command '], stdout=subprocess.PIPE).communicate()[0]
268 processes = ps.decode("utf-8") 312 processes = ps.decode("utf-8")
269 self.logger.debug("Running processes:\n%s" % processes) 313 self.logger.debug("Running processes:\n%s" % processes)
270 self._dump_host() 314 op = getOutput(output)
271 op = self.getOutput(output)
272 self.stop() 315 self.stop()
273 if op: 316 if op:
274 self.logger.error("Output from runqemu:\n%s" % op) 317 self.logger.error("Output from runqemu:\n%s" % op)
@@ -276,10 +319,79 @@ class QemuRunner:
276 self.logger.error("No output from runqemu.\n") 319 self.logger.error("No output from runqemu.\n")
277 return False 320 return False
278 321
322 # Create the client socket for the QEMU Monitor Control Socket
323 # This will allow us to read status from Qemu if the the process
324 # is still alive
325 self.logger.debug("QMP Initializing to %s" % (qmp_port))
326 # chdir dance for path length issues with unix sockets
327 origpath = os.getcwd()
328 try:
329 os.chdir(os.path.dirname(qmp_port))
330 try:
331 from qmp.legacy import QEMUMonitorProtocol
332 self.qmp = QEMUMonitorProtocol(os.path.basename(qmp_port))
333 except OSError as msg:
334 self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
335 return False
336
337 self.logger.debug("QMP Connecting to %s" % (qmp_port))
338 if not os.path.exists(qmp_port) and self.is_alive():
339 self.logger.debug("QMP Port does not exist waiting for it to be created")
340 endtime = time.time() + self.runqemutime
341 while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
342 self.logger.info("QMP port does not exist yet!")
343 time.sleep(0.5)
344 if not os.path.exists(qmp_port) and self.is_alive():
345 self.logger.warning("QMP Port still does not exist but QEMU is alive")
346 return False
347
348 try:
349 # set timeout value for all QMP calls
350 self.qmp.settimeout(self.runqemutime)
351 self.qmp.connect()
352 connect_time = time.time()
353 self.logger.info("QMP connected to QEMU at %s and took %.2f seconds" %
354 (time.strftime("%D %H:%M:%S"),
355 time.time() - launch_time))
356 except OSError as msg:
357 self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
358 return False
359 except qmp.legacy.QMPError as msg:
360 self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
361 return False
362 finally:
363 os.chdir(origpath)
364
365 # We worry that mmap'd libraries may cause page faults which hang the qemu VM for periods
366 # causing failures. Before we "start" qemu, read through it's mapped files to try and
367 # ensure we don't hit page faults later
368 mapdir = "/proc/" + str(self.qemupid) + "/map_files/"
369 try:
370 for f in os.listdir(mapdir):
371 try:
372 linktarget = os.readlink(os.path.join(mapdir, f))
373 if not linktarget.startswith("/") or linktarget.startswith("/dev") or "deleted" in linktarget:
374 continue
375 with open(linktarget, "rb") as readf:
376 data = True
377 while data:
378 data = readf.read(4096)
379 except FileNotFoundError:
380 continue
381 # Centos7 doesn't allow us to read /map_files/
382 except PermissionError:
383 pass
384
385 # Release the qemu process to continue running
386 self.run_monitor('cont')
387 self.logger.info("QMP released QEMU at %s and took %.2f seconds from connect" %
388 (time.strftime("%D %H:%M:%S"),
389 time.time() - connect_time))
390
279 # We are alive: qemu is running 391 # We are alive: qemu is running
280 out = self.getOutput(output) 392 out = getOutput(output)
281 netconf = False # network configuration is not required by default 393 netconf = False # network configuration is not required by default
282 self.logger.debug("qemu started in %s seconds - qemu procces pid is %s (%s)" % 394 self.logger.debug("qemu started in %.2f seconds - qemu procces pid is %s (%s)" %
283 (time.time() - (endtime - self.runqemutime), 395 (time.time() - (endtime - self.runqemutime),
284 self.qemupid, time.strftime("%D %H:%M:%S"))) 396 self.qemupid, time.strftime("%D %H:%M:%S")))
285 cmdline = '' 397 cmdline = ''
@@ -291,9 +403,10 @@ class QemuRunner:
291 cmdline = re_control_char.sub(' ', cmdline) 403 cmdline = re_control_char.sub(' ', cmdline)
292 try: 404 try:
293 if self.use_slirp: 405 if self.use_slirp:
294 tcp_ports = cmdline.split("hostfwd=tcp::")[1] 406 tcp_ports = cmdline.split("hostfwd=tcp:")[1]
407 ip, tcp_ports = tcp_ports.split(":")[:2]
295 host_port = tcp_ports[:tcp_ports.find('-')] 408 host_port = tcp_ports[:tcp_ports.find('-')]
296 self.ip = "localhost:%s" % host_port 409 self.ip = "%s:%s" % (ip, host_port)
297 else: 410 else:
298 ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1]) 411 ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
299 self.ip = ips[0] 412 self.ip = ips[0]
@@ -301,8 +414,8 @@ class QemuRunner:
301 self.logger.debug("qemu cmdline used:\n{}".format(cmdline)) 414 self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
302 except (IndexError, ValueError): 415 except (IndexError, ValueError):
303 # Try to get network configuration from runqemu output 416 # Try to get network configuration from runqemu output
304 match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+)$.*', 417 match = re.match(r'.*Network configuration: (?:ip=)*([0-9.]+)::([0-9.]+):([0-9.]+).*',
305 out, re.MULTILINE|re.DOTALL) 418 out, re.MULTILINE | re.DOTALL)
306 if match: 419 if match:
307 self.ip, self.server_ip, self.netmask = match.groups() 420 self.ip, self.server_ip, self.netmask = match.groups()
308 # network configuration is required as we couldn't get it 421 # network configuration is required as we couldn't get it
@@ -313,16 +426,16 @@ class QemuRunner:
313 self.logger.error("Couldn't get ip from qemu command line and runqemu output! " 426 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" 427 "Here is the qemu command line used:\n%s\n"
315 "and output from runqemu:\n%s" % (cmdline, out)) 428 "and output from runqemu:\n%s" % (cmdline, out))
316 self._dump_host()
317 self.stop() 429 self.stop()
318 return False 430 return False
319 431
320 self.logger.debug("Target IP: %s" % self.ip) 432 self.logger.debug("Target IP: %s" % self.ip)
321 self.logger.debug("Server IP: %s" % self.server_ip) 433 self.logger.debug("Server IP: %s" % self.server_ip)
322 434
435 self.thread = LoggingThread(self.log, self.threadsock, self.logger, self.runqemu.stdout)
436 self.thread.start()
437
323 if self.serial_ports >= 2: 438 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): 439 if not self.thread.connection_established.wait(self.boottime):
327 self.logger.error("Didn't receive a console connection from qemu. " 440 self.logger.error("Didn't receive a console connection from qemu. "
328 "Here is the qemu command line used:\n%s\nand " 441 "Here is the qemu command line used:\n%s\nand "
@@ -334,7 +447,7 @@ class QemuRunner:
334 self.logger.debug("Waiting at most %d seconds for login banner (%s)" % 447 self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
335 (self.boottime, time.strftime("%D %H:%M:%S"))) 448 (self.boottime, time.strftime("%D %H:%M:%S")))
336 endtime = time.time() + self.boottime 449 endtime = time.time() + self.boottime
337 socklist = [self.server_socket] 450 filelist = [self.server_socket]
338 reachedlogin = False 451 reachedlogin = False
339 stopread = False 452 stopread = False
340 qemusock = None 453 qemusock = None
@@ -342,61 +455,82 @@ class QemuRunner:
342 data = b'' 455 data = b''
343 while time.time() < endtime and not stopread: 456 while time.time() < endtime and not stopread:
344 try: 457 try:
345 sread, swrite, serror = select.select(socklist, [], [], 5) 458 sread, swrite, serror = select.select(filelist, [], [], 5)
346 except InterruptedError: 459 except InterruptedError:
347 continue 460 continue
348 for sock in sread: 461 for file in sread:
349 if sock is self.server_socket: 462 if file is self.server_socket:
350 qemusock, addr = self.server_socket.accept() 463 qemusock, addr = self.server_socket.accept()
351 qemusock.setblocking(0) 464 qemusock.setblocking(False)
352 socklist.append(qemusock) 465 filelist.append(qemusock)
353 socklist.remove(self.server_socket) 466 filelist.remove(self.server_socket)
354 self.logger.debug("Connection from %s:%s" % addr) 467 self.logger.debug("Connection from %s:%s" % addr)
355 else: 468 else:
356 data = data + sock.recv(1024) 469 # try to avoid reading only a single character at a time
470 time.sleep(0.1)
471 if hasattr(file, 'read'):
472 read = file.read(1024)
473 elif hasattr(file, 'recv'):
474 read = file.recv(1024)
475 else:
476 self.logger.error('Invalid file type: %s\n%s' % (file))
477 read = b''
478
479 self.logger.debug2('Partial boot log:\n%s' % (read.decode('utf-8', errors='backslashreplace')))
480 data = data + read
357 if data: 481 if data:
358 bootlog += data 482 bootlog += data
359 if self.serial_ports < 2: 483 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'' 484 data = b''
364 if self.boot_patterns['search_reached_prompt'] in bootlog: 485
486 if bytes(self.boot_patterns['search_reached_prompt'], 'utf-8') in bootlog:
487 self.server_socket.close()
365 self.server_socket = qemusock 488 self.server_socket = qemusock
366 stopread = True 489 stopread = True
367 reachedlogin = True 490 reachedlogin = True
368 self.logger.debug("Reached login banner in %s seconds (%s)" % 491 self.logger.debug("Reached login banner in %.2f seconds (%s)" %
369 (time.time() - (endtime - self.boottime), 492 (time.time() - (endtime - self.boottime),
370 time.strftime("%D %H:%M:%S"))) 493 time.strftime("%D %H:%M:%S")))
371 else: 494 else:
372 # no need to check if reachedlogin unless we support multiple connections 495 # no need to check if reachedlogin unless we support multiple connections
373 self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" % 496 self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
374 time.strftime("%D %H:%M:%S")) 497 time.strftime("%D %H:%M:%S"))
375 socklist.remove(sock) 498 filelist.remove(file)
376 sock.close() 499 file.close()
377 stopread = True 500 stopread = True
378 501
379
380 if not reachedlogin: 502 if not reachedlogin:
381 if time.time() >= endtime: 503 if time.time() >= endtime:
382 self.logger.warning("Target didn't reach login banner in %d seconds (%s)" % 504 self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
383 (self.boottime, time.strftime("%D %H:%M:%S"))) 505 (self.boottime, time.strftime("%D %H:%M:%S")))
384 tail = lambda l: "\n".join(l.splitlines()[-25:]) 506 tail = lambda l: "\n".join(l.splitlines()[-25:])
385 bootlog = bootlog.decode("utf-8") 507 bootlog = self.decode_qemulog(bootlog)
386 # in case bootlog is empty, use tail qemu log store at self.msg 508 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) 509 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) 510 self.logger.warning("Check full boot log: %s" % self.logfile)
390 self._dump_host()
391 self.stop() 511 self.stop()
512 data = True
513 while data:
514 try:
515 time.sleep(1)
516 data = qemusock.recv(1024)
517 self.log(data, extension = ".2")
518 self.logger.warning('Extra log data read: %s\n' % (data.decode('utf-8', errors='backslashreplace')))
519 except Exception as e:
520 self.logger.warning('Extra log data exception %s' % repr(e))
521 data = None
522 self.thread.serial_lock.release()
392 return False 523 return False
393 524
525 with self.thread.serial_lock:
526 self.thread.set_serialsock(self.server_socket)
527
394 # If we are not able to login the tests can continue 528 # If we are not able to login the tests can continue
395 try: 529 try:
396 (status, output) = self.run_serial(self.boot_patterns['send_login_user'], raw=True, timeout=120) 530 (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): 531 if re.search(self.boot_patterns['search_login_succeeded'], output):
398 self.logged = True 532 self.logged = True
399 self.logger.debug("Logged as root in serial console") 533 self.logger.debug("Logged in as %s in serial console" % self.boot_patterns['send_login_user'].replace("\n", ""))
400 if netconf: 534 if netconf:
401 # configure guest networking 535 # configure guest networking
402 cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask) 536 cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
@@ -407,7 +541,7 @@ class QemuRunner:
407 self.logger.debug("Couldn't configure guest networking") 541 self.logger.debug("Couldn't configure guest networking")
408 else: 542 else:
409 self.logger.warning("Couldn't login into serial console" 543 self.logger.warning("Couldn't login into serial console"
410 " as root using blank password") 544 " as %s using blank password" % self.boot_patterns['send_login_user'].replace("\n", ""))
411 self.logger.warning("The output:\n%s" % output) 545 self.logger.warning("The output:\n%s" % output)
412 except: 546 except:
413 self.logger.warning("Serial console failed while trying to login") 547 self.logger.warning("Serial console failed while trying to login")
@@ -427,16 +561,24 @@ class QemuRunner:
427 except OSError as e: 561 except OSError as e:
428 if e.errno != errno.ESRCH: 562 if e.errno != errno.ESRCH:
429 raise 563 raise
430 endtime = time.time() + self.runqemutime 564 try:
431 while self.runqemu.poll() is None and time.time() < endtime: 565 outs, errs = self.runqemu.communicate(timeout=self.runqemutime)
432 time.sleep(1) 566 if outs:
433 if self.runqemu.poll() is None: 567 self.logger.info("Output from runqemu:\n%s", outs.decode("utf-8"))
568 if errs:
569 self.logger.info("Stderr from runqemu:\n%s", errs.decode("utf-8"))
570 except subprocess.TimeoutExpired:
434 self.logger.debug("Sending SIGKILL to runqemu") 571 self.logger.debug("Sending SIGKILL to runqemu")
435 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL) 572 os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
573 if not self.runqemu.stdout.closed:
574 self.logger.info("Output from runqemu:\n%s" % getOutput(self.runqemu.stdout))
436 self.runqemu.stdin.close() 575 self.runqemu.stdin.close()
437 self.runqemu.stdout.close() 576 self.runqemu.stdout.close()
438 self.runqemu_exited = True 577 self.runqemu_exited = True
439 578
579 if hasattr(self, 'qmp') and self.qmp:
580 self.qmp.close()
581 self.qmp = None
440 if hasattr(self, 'server_socket') and self.server_socket: 582 if hasattr(self, 'server_socket') and self.server_socket:
441 self.server_socket.close() 583 self.server_socket.close()
442 self.server_socket = None 584 self.server_socket = None
@@ -467,6 +609,11 @@ class QemuRunner:
467 self.thread.stop() 609 self.thread.stop()
468 self.thread.join() 610 self.thread.join()
469 611
612 def allowexit(self):
613 self.canexit = True
614 if self.thread:
615 self.thread.allowexit()
616
470 def restart(self, qemuparams = None): 617 def restart(self, qemuparams = None):
471 self.logger.warning("Restarting qemu process") 618 self.logger.warning("Restarting qemu process")
472 if self.runqemu.poll() is None: 619 if self.runqemu.poll() is None:
@@ -483,8 +630,12 @@ class QemuRunner:
483 # so it's possible that the file has been created but the content is empty 630 # so it's possible that the file has been created but the content is empty
484 pidfile_timeout = time.time() + 3 631 pidfile_timeout = time.time() + 3
485 while time.time() < pidfile_timeout: 632 while time.time() < pidfile_timeout:
486 with open(self.qemu_pidfile, 'r') as f: 633 try:
487 qemu_pid = f.read().strip() 634 with open(self.qemu_pidfile, 'r') as f:
635 qemu_pid = f.read().strip()
636 except FileNotFoundError:
637 # Can be used to detect shutdown so the pid file can disappear
638 return False
488 # file created but not yet written contents 639 # file created but not yet written contents
489 if not qemu_pid: 640 if not qemu_pid:
490 time.sleep(0.5) 641 time.sleep(0.5)
@@ -495,34 +646,49 @@ class QemuRunner:
495 return True 646 return True
496 return False 647 return False
497 648
649 def run_monitor(self, command, args=None, timeout=60):
650 if hasattr(self, 'qmp') and self.qmp:
651 self.qmp.settimeout(timeout)
652 if args is not None:
653 return self.qmp.cmd_raw(command, args)
654 else:
655 return self.qmp.cmd_raw(command)
656
498 def run_serial(self, command, raw=False, timeout=60): 657 def run_serial(self, command, raw=False, timeout=60):
658 # Returns (status, output) where status is 1 on success and 0 on error
659
499 # We assume target system have echo to get command status 660 # We assume target system have echo to get command status
500 if not raw: 661 if not raw:
501 command = "%s; echo $?\n" % command 662 command = "%s; echo $?\n" % command
502 663
503 data = '' 664 data = ''
504 status = 0 665 status = 0
505 self.server_socket.sendall(command.encode('utf-8')) 666 with self.thread.serial_lock:
506 start = time.time() 667 self.server_socket.sendall(command.encode('utf-8'))
507 end = start + timeout 668 start = time.time()
508 while True: 669 end = start + timeout
509 now = time.time() 670 while True:
510 if now >= end: 671 now = time.time()
511 data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout 672 if now >= end:
512 break 673 data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
513 try: 674 break
514 sread, _, _ = select.select([self.server_socket],[],[], end - now) 675 try:
515 except InterruptedError: 676 sread, _, _ = select.select([self.server_socket],[],[], end - now)
516 continue 677 except InterruptedError:
517 if sread: 678 continue
518 answer = self.server_socket.recv(1024) 679 if sread:
519 if answer: 680 # try to avoid reading single character at a time
520 data += answer.decode('utf-8') 681 time.sleep(0.1)
521 # Search the prompt to stop 682 answer = self.server_socket.recv(1024)
522 if re.search(self.boot_patterns['search_cmd_finished'], data): 683 if answer:
523 break 684 data += answer.decode('utf-8')
524 else: 685 # Search the prompt to stop
525 raise Exception("No data on serial console socket") 686 if re.search(self.boot_patterns['search_cmd_finished'], data):
687 break
688 else:
689 if self.canexit:
690 return (1, "")
691 raise Exception("No data on serial console socket, connection closed?")
526 692
527 if data: 693 if data:
528 if raw: 694 if raw:
@@ -541,34 +707,46 @@ class QemuRunner:
541 status = 1 707 status = 1
542 return (status, str(data)) 708 return (status, str(data))
543 709
544 710@contextmanager
545 def _dump_host(self): 711def nonblocking_lock(lock):
546 self.host_dumper.create_dir("qemu") 712 locked = lock.acquire(False)
547 self.logger.warning("Qemu ended unexpectedly, dump data from host" 713 try:
548 " is in %s" % self.host_dumper.dump_dir) 714 yield locked
549 self.host_dumper.dump_host() 715 finally:
716 if locked:
717 lock.release()
550 718
551# This class is for reading data from a socket and passing it to logfunc 719# 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 720# 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 721# 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. 722# will wake up the poll and allow for tearing everything down.
555class LoggingThread(threading.Thread): 723class LoggingThread(threading.Thread):
556 def __init__(self, logfunc, sock, logger): 724 def __init__(self, logfunc, sock, logger, qemuoutput):
557 self.connection_established = threading.Event() 725 self.connection_established = threading.Event()
726 self.serial_lock = threading.Lock()
727
558 self.serversock = sock 728 self.serversock = sock
729 self.serialsock = None
730 self.qemuoutput = qemuoutput
559 self.logfunc = logfunc 731 self.logfunc = logfunc
560 self.logger = logger 732 self.logger = logger
561 self.readsock = None 733 self.readsock = None
562 self.running = False 734 self.running = False
735 self.canexit = False
563 736
564 self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL 737 self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
565 self.readevents = select.POLLIN | select.POLLPRI 738 self.readevents = select.POLLIN | select.POLLPRI
566 739
567 threading.Thread.__init__(self, target=self.threadtarget) 740 threading.Thread.__init__(self, target=self.threadtarget)
568 741
742 def set_serialsock(self, serialsock):
743 self.serialsock = serialsock
744
569 def threadtarget(self): 745 def threadtarget(self):
570 try: 746 try:
571 self.eventloop() 747 self.eventloop()
748 except Exception as e:
749 self.logger.warning("Exception %s in logging thread" % traceback.format_exception(e))
572 finally: 750 finally:
573 self.teardown() 751 self.teardown()
574 752
@@ -584,7 +762,8 @@ class LoggingThread(threading.Thread):
584 762
585 def teardown(self): 763 def teardown(self):
586 self.logger.debug("Tearing down logging thread") 764 self.logger.debug("Tearing down logging thread")
587 self.close_socket(self.serversock) 765 if self.serversock:
766 self.close_socket(self.serversock)
588 767
589 if self.readsock is not None: 768 if self.readsock is not None:
590 self.close_socket(self.readsock) 769 self.close_socket(self.readsock)
@@ -593,30 +772,37 @@ class LoggingThread(threading.Thread):
593 self.close_ignore_error(self.writepipe) 772 self.close_ignore_error(self.writepipe)
594 self.running = False 773 self.running = False
595 774
775 def allowexit(self):
776 self.canexit = True
777
596 def eventloop(self): 778 def eventloop(self):
597 poll = select.poll() 779 poll = select.poll()
598 event_read_mask = self.errorevents | self.readevents 780 event_read_mask = self.errorevents | self.readevents
599 poll.register(self.serversock.fileno()) 781 if self.serversock:
782 poll.register(self.serversock.fileno())
783 serial_registered = False
784 poll.register(self.qemuoutput.fileno())
600 poll.register(self.readpipe, event_read_mask) 785 poll.register(self.readpipe, event_read_mask)
601 786
602 breakout = False 787 breakout = False
603 self.running = True 788 self.running = True
604 self.logger.debug("Starting thread event loop") 789 self.logger.debug("Starting thread event loop")
605 while not breakout: 790 while not breakout:
606 events = poll.poll() 791 events = poll.poll(2)
607 for event in events: 792 for fd, event in events:
793
608 # An error occurred, bail out 794 # An error occurred, bail out
609 if event[1] & self.errorevents: 795 if event & self.errorevents:
610 raise Exception(self.stringify_event(event[1])) 796 raise Exception(self.stringify_event(event))
611 797
612 # Event to stop the thread 798 # Event to stop the thread
613 if self.readpipe == event[0]: 799 if self.readpipe == fd:
614 self.logger.debug("Stop event received") 800 self.logger.debug("Stop event received")
615 breakout = True 801 breakout = True
616 break 802 break
617 803
618 # A connection request was received 804 # A connection request was received
619 elif self.serversock.fileno() == event[0]: 805 elif self.serversock and self.serversock.fileno() == fd:
620 self.logger.debug("Connection request received") 806 self.logger.debug("Connection request received")
621 self.readsock, _ = self.serversock.accept() 807 self.readsock, _ = self.serversock.accept()
622 self.readsock.setblocking(0) 808 self.readsock.setblocking(0)
@@ -627,18 +813,38 @@ class LoggingThread(threading.Thread):
627 self.connection_established.set() 813 self.connection_established.set()
628 814
629 # Actual data to be logged 815 # Actual data to be logged
630 elif self.readsock.fileno() == event[0]: 816 elif self.readsock and self.readsock.fileno() == fd:
631 data = self.recv(1024) 817 data = self.recv(1024, self.readsock)
632 self.logfunc(data) 818 self.logfunc(data)
819 elif self.qemuoutput.fileno() == fd:
820 data = self.qemuoutput.read()
821 self.logger.debug("Data received on qemu stdout %s" % data)
822 self.logfunc(data, ".stdout")
823 elif self.serialsock and self.serialsock.fileno() == fd:
824 if self.serial_lock.acquire(blocking=False):
825 data = self.recv(1024, self.serialsock)
826 self.logger.debug("Data received serial thread %s" % data.decode('utf-8', 'replace'))
827 self.logfunc(data, ".2")
828 self.serial_lock.release()
829 else:
830 serial_registered = False
831 poll.unregister(self.serialsock.fileno())
832
833 if not serial_registered and self.serialsock:
834 with nonblocking_lock(self.serial_lock) as l:
835 if l:
836 serial_registered = True
837 poll.register(self.serialsock.fileno(), event_read_mask)
838
633 839
634 # Since the socket is non-blocking make sure to honor EAGAIN 840 # Since the socket is non-blocking make sure to honor EAGAIN
635 # and EWOULDBLOCK. 841 # and EWOULDBLOCK.
636 def recv(self, count): 842 def recv(self, count, sock):
637 try: 843 try:
638 data = self.readsock.recv(count) 844 data = sock.recv(count)
639 except socket.error as e: 845 except socket.error as e:
640 if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK: 846 if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
641 return '' 847 return b''
642 else: 848 else:
643 raise 849 raise
644 850
@@ -649,7 +855,9 @@ class LoggingThread(threading.Thread):
649 # happened. But for this code it counts as an 855 # happened. But for this code it counts as an
650 # error since the connection shouldn't go away 856 # error since the connection shouldn't go away
651 # until qemu exits. 857 # until qemu exits.
652 raise Exception("Console connection closed unexpectedly") 858 if not self.canexit:
859 raise Exception("Console connection closed unexpectedly")
860 return b''
653 861
654 return data 862 return data
655 863
@@ -661,6 +869,9 @@ class LoggingThread(threading.Thread):
661 val = 'POLLHUP' 869 val = 'POLLHUP'
662 elif select.POLLNVAL == event: 870 elif select.POLLNVAL == event:
663 val = 'POLLNVAL' 871 val = 'POLLNVAL'
872 else:
873 val = "0x%x" % (event)
874
664 return val 875 return val
665 876
666 def close_socket(self, sock): 877 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/subprocesstweak.py b/meta/lib/oeqa/utils/subprocesstweak.py
index b47975a4bc..3e43ed547b 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
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