blob: 5cd0f7477baa5bb45f2b2b5b93fb1ff0efd02923 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Copyright (c) 2013-2014 Intel Corporation
2#
3# Released under the MIT license (see COPYING.MIT)
4
5# DESCRIPTION
6# This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest
7# It provides a class and methods for running commands on the host in a convienent way for tests.
8
9
10
11import os
12import sys
13import signal
14import subprocess
15import threading
16import logging
17from oeqa.utils import CommandError
18from oeqa.utils import ftools
19import re
20import contextlib
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050021# Export test doesn't require bb
22try:
23 import bb
24except ImportError:
25 pass
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026
27class Command(object):
28 def __init__(self, command, bg=False, timeout=None, data=None, **options):
29
30 self.defaultopts = {
31 "stdout": subprocess.PIPE,
32 "stderr": subprocess.STDOUT,
33 "stdin": None,
34 "shell": False,
35 "bufsize": -1,
36 }
37
38 self.cmd = command
39 self.bg = bg
40 self.timeout = timeout
41 self.data = data
42
43 self.options = dict(self.defaultopts)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060044 if isinstance(self.cmd, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050045 self.options["shell"] = True
46 if self.data:
47 self.options['stdin'] = subprocess.PIPE
48 self.options.update(options)
49
50 self.status = None
51 self.output = None
52 self.error = None
53 self.thread = None
54
55 self.log = logging.getLogger("utils.commands")
56
57 def run(self):
58 self.process = subprocess.Popen(self.cmd, **self.options)
59
60 def commThread():
61 self.output, self.error = self.process.communicate(self.data)
62
63 self.thread = threading.Thread(target=commThread)
64 self.thread.start()
65
66 self.log.debug("Running command '%s'" % self.cmd)
67
68 if not self.bg:
69 self.thread.join(self.timeout)
70 self.stop()
71
72 def stop(self):
73 if self.thread.isAlive():
74 self.process.terminate()
75 # let's give it more time to terminate gracefully before killing it
76 self.thread.join(5)
77 if self.thread.isAlive():
78 self.process.kill()
79 self.thread.join()
80
Patrick Williamsc0f7c042017-02-23 20:41:17 -060081 if not self.output:
82 self.output = ""
83 else:
84 self.output = self.output.decode("utf-8", errors='replace').rstrip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050085 self.status = self.process.poll()
86
87 self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status))
88 # logging the complete output is insane
89 # bitbake -e output is really big
90 # and makes the log file useless
91 if self.status:
92 lout = "\n".join(self.output.splitlines()[-20:])
93 self.log.debug("Last 20 lines:\n%s" % lout)
94
95
96class Result(object):
97 pass
98
99
100def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options):
101 result = Result()
102
103 cmd = Command(command, timeout=timeout, **options)
104 cmd.run()
105
106 result.command = command
107 result.status = cmd.status
108 result.output = cmd.output
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600109 result.error = cmd.error
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500110 result.pid = cmd.process.pid
111
112 if result.status and not ignore_status:
113 if assert_error:
114 raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output))
115 else:
116 raise CommandError(result.status, command, result.output)
117
118 return result
119
120
121def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options):
122
123 if postconfig:
124 postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf')
125 ftools.write_file(postconfig_file, postconfig)
126 extra_args = "-R %s" % postconfig_file
127 else:
128 extra_args = ""
129
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600130 if isinstance(command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500131 cmd = "bitbake " + extra_args + " " + command
132 else:
133 cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]]
134
135 try:
136 return runCmd(cmd, ignore_status, timeout, **options)
137 finally:
138 if postconfig:
139 os.remove(postconfig_file)
140
141
142def get_bb_env(target=None, postconfig=None):
143 if target:
144 return bitbake("-e %s" % target, postconfig=postconfig).output
145 else:
146 return bitbake("-e", postconfig=postconfig).output
147
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600148def get_bb_vars(variables=None, target=None, postconfig=None):
149 """Get values of multiple bitbake variables"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500150 bbenv = get_bb_env(target, postconfig=postconfig)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600151
152 var_re = re.compile(r'^(export )?(?P<var>\w+)="(?P<value>.*)"$')
153 unset_re = re.compile(r'^unset (?P<var>\w+)$')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500154 lastline = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600155 values = {}
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500156 for line in bbenv.splitlines():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600157 match = var_re.match(line)
158 val = None
159 if match:
160 val = match.group('value')
161 else:
162 match = unset_re.match(line)
163 if match:
164 # Handle [unexport] variables
165 if lastline.startswith('# "'):
166 val = lastline.split('"')[1]
167 if val:
168 var = match.group('var')
169 if variables is None:
170 values[var] = val
171 else:
172 if var in variables:
173 values[var] = val
174 variables.remove(var)
175 # Stop after all required variables have been found
176 if not variables:
177 break
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500178 lastline = line
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600179 if variables:
180 # Fill in missing values
181 for var in variables:
182 values[var] = None
183 return values
184
185def get_bb_var(var, target=None, postconfig=None):
186 return get_bb_vars([var], target, postconfig)[var]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500187
188def get_test_layer():
189 layers = get_bb_var("BBLAYERS").split()
190 testlayer = None
191 for l in layers:
192 if '~' in l:
193 l = os.path.expanduser(l)
194 if "/meta-selftest" in l and os.path.isdir(l):
195 testlayer = l
196 break
197 return testlayer
198
199def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
200 os.makedirs(os.path.join(templayerdir, 'conf'))
201 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
202 f.write('BBPATH .= ":${LAYERDIR}"\n')
203 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
204 f.write(' ${LAYERDIR}/%s/*.bbappend"\n' % recipepathspec)
205 f.write('BBFILE_COLLECTIONS += "%s"\n' % templayername)
206 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
207 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
208 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
209
210
211@contextlib.contextmanager
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500212def runqemu(pn, ssh=True):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500213
214 import bb.tinfoil
215 import bb.build
216
217 tinfoil = bb.tinfoil.Tinfoil()
218 tinfoil.prepare(False)
219 try:
220 tinfoil.logger.setLevel(logging.WARNING)
221 import oeqa.targetcontrol
222 tinfoil.config_data.setVar("TEST_LOG_DIR", "${WORKDIR}/testimage")
223 tinfoil.config_data.setVar("TEST_QEMUBOOT_TIMEOUT", "1000")
224 import oe.recipeutils
225 recipefile = oe.recipeutils.pn_to_recipe(tinfoil.cooker, pn)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600226 recipedata = oe.recipeutils.parse_recipe(tinfoil.cooker, recipefile, [])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227
228 # The QemuRunner log is saved out, but we need to ensure it is at the right
229 # log level (and then ensure that since it's a child of the BitBake logger,
230 # we disable propagation so we don't then see the log events on the console)
231 logger = logging.getLogger('BitBake.QemuRunner')
232 logger.setLevel(logging.DEBUG)
233 logger.propagate = False
234 logdir = recipedata.getVar("TEST_LOG_DIR", True)
235
236 qemu = oeqa.targetcontrol.QemuTarget(recipedata)
237 finally:
238 # We need to shut down tinfoil early here in case we actually want
239 # to run tinfoil-using utilities with the running QEMU instance.
240 # Luckily QemuTarget doesn't need it after the constructor.
241 tinfoil.shutdown()
242
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500243 # Setup bitbake logger as console handler is removed by tinfoil.shutdown
244 bblogger = logging.getLogger('BitBake')
245 bblogger.setLevel(logging.INFO)
246 console = logging.StreamHandler(sys.stdout)
247 bbformat = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
248 if sys.stdout.isatty():
249 bbformat.enable_color()
250 console.setFormatter(bbformat)
251 bblogger.addHandler(console)
252
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500253 try:
254 qemu.deploy()
255 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500256 qemu.start(ssh=ssh)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500257 except bb.build.FuncFailed:
258 raise Exception('Failed to start QEMU - see the logs in %s' % logdir)
259
260 yield qemu
261
262 finally:
263 try:
264 qemu.stop()
265 except:
266 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600267
268def updateEnv(env_file):
269 """
270 Source a file and update environment.
271 """
272
273 cmd = ". %s; env -0" % env_file
274 result = runCmd(cmd)
275
276 for line in result.output.split("\0"):
277 (key, _, value) = line.partition("=")
278 os.environ[key] = value