blob: 48f64412901f2cc9d46b42dcfba119eae52f02df [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)
44 if isinstance(self.cmd, basestring):
45 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
81 self.output = self.output.rstrip()
82 self.status = self.process.poll()
83
84 self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status))
85 # logging the complete output is insane
86 # bitbake -e output is really big
87 # and makes the log file useless
88 if self.status:
89 lout = "\n".join(self.output.splitlines()[-20:])
90 self.log.debug("Last 20 lines:\n%s" % lout)
91
92
93class Result(object):
94 pass
95
96
97def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options):
98 result = Result()
99
100 cmd = Command(command, timeout=timeout, **options)
101 cmd.run()
102
103 result.command = command
104 result.status = cmd.status
105 result.output = cmd.output
106 result.pid = cmd.process.pid
107
108 if result.status and not ignore_status:
109 if assert_error:
110 raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output))
111 else:
112 raise CommandError(result.status, command, result.output)
113
114 return result
115
116
117def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options):
118
119 if postconfig:
120 postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf')
121 ftools.write_file(postconfig_file, postconfig)
122 extra_args = "-R %s" % postconfig_file
123 else:
124 extra_args = ""
125
126 if isinstance(command, basestring):
127 cmd = "bitbake " + extra_args + " " + command
128 else:
129 cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]]
130
131 try:
132 return runCmd(cmd, ignore_status, timeout, **options)
133 finally:
134 if postconfig:
135 os.remove(postconfig_file)
136
137
138def get_bb_env(target=None, postconfig=None):
139 if target:
140 return bitbake("-e %s" % target, postconfig=postconfig).output
141 else:
142 return bitbake("-e", postconfig=postconfig).output
143
144def get_bb_var(var, target=None, postconfig=None):
145 val = None
146 bbenv = get_bb_env(target, postconfig=postconfig)
147 lastline = None
148 for line in bbenv.splitlines():
149 if re.search("^(export )?%s=" % var, line):
150 val = line.split('=', 1)[1]
151 val = val.strip('\"')
152 break
153 elif re.match("unset %s$" % var, line):
154 # Handle [unexport] variables
155 if lastline.startswith('# "'):
156 val = lastline.split('\"')[1]
157 break
158 lastline = line
159 return val
160
161def get_test_layer():
162 layers = get_bb_var("BBLAYERS").split()
163 testlayer = None
164 for l in layers:
165 if '~' in l:
166 l = os.path.expanduser(l)
167 if "/meta-selftest" in l and os.path.isdir(l):
168 testlayer = l
169 break
170 return testlayer
171
172def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
173 os.makedirs(os.path.join(templayerdir, 'conf'))
174 with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
175 f.write('BBPATH .= ":${LAYERDIR}"\n')
176 f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
177 f.write(' ${LAYERDIR}/%s/*.bbappend"\n' % recipepathspec)
178 f.write('BBFILE_COLLECTIONS += "%s"\n' % templayername)
179 f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
180 f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
181 f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
182
183
184@contextlib.contextmanager
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500185def runqemu(pn, ssh=True):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500186
187 import bb.tinfoil
188 import bb.build
189
190 tinfoil = bb.tinfoil.Tinfoil()
191 tinfoil.prepare(False)
192 try:
193 tinfoil.logger.setLevel(logging.WARNING)
194 import oeqa.targetcontrol
195 tinfoil.config_data.setVar("TEST_LOG_DIR", "${WORKDIR}/testimage")
196 tinfoil.config_data.setVar("TEST_QEMUBOOT_TIMEOUT", "1000")
197 import oe.recipeutils
198 recipefile = oe.recipeutils.pn_to_recipe(tinfoil.cooker, pn)
199 recipedata = oe.recipeutils.parse_recipe(recipefile, [], tinfoil.config_data)
200
201 # The QemuRunner log is saved out, but we need to ensure it is at the right
202 # log level (and then ensure that since it's a child of the BitBake logger,
203 # we disable propagation so we don't then see the log events on the console)
204 logger = logging.getLogger('BitBake.QemuRunner')
205 logger.setLevel(logging.DEBUG)
206 logger.propagate = False
207 logdir = recipedata.getVar("TEST_LOG_DIR", True)
208
209 qemu = oeqa.targetcontrol.QemuTarget(recipedata)
210 finally:
211 # We need to shut down tinfoil early here in case we actually want
212 # to run tinfoil-using utilities with the running QEMU instance.
213 # Luckily QemuTarget doesn't need it after the constructor.
214 tinfoil.shutdown()
215
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500216 # Setup bitbake logger as console handler is removed by tinfoil.shutdown
217 bblogger = logging.getLogger('BitBake')
218 bblogger.setLevel(logging.INFO)
219 console = logging.StreamHandler(sys.stdout)
220 bbformat = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
221 if sys.stdout.isatty():
222 bbformat.enable_color()
223 console.setFormatter(bbformat)
224 bblogger.addHandler(console)
225
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500226 try:
227 qemu.deploy()
228 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500229 qemu.start(ssh=ssh)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500230 except bb.build.FuncFailed:
231 raise Exception('Failed to start QEMU - see the logs in %s' % logdir)
232
233 yield qemu
234
235 finally:
236 try:
237 qemu.stop()
238 except:
239 pass