Jürg Billeter pushed to branch juerg/command-batching at BuildStream / buildstream
Commits:
-
516e990e
by ctolentino8 at 2018-10-31T11:36:46Z
-
b8a37a63
by Tristan Van Berkom at 2018-11-01T10:16:25Z
-
10d8d994
by Jürg Billeter at 2018-11-01T11:00:08Z
-
15d56e06
by Jürg Billeter at 2018-11-01T11:00:08Z
-
59b436df
by Jürg Billeter at 2018-11-01T11:00:08Z
-
ecd9d5e4
by Jürg Billeter at 2018-11-01T11:00:08Z
-
1f16d9fd
by Jürg Billeter at 2018-11-01T11:00:08Z
-
e1e4234d
by Jürg Billeter at 2018-11-01T11:00:08Z
-
ca6f10ce
by Jürg Billeter at 2018-11-01T11:00:08Z
-
2fbf0c6d
by Jürg Billeter at 2018-11-01T11:00:08Z
-
7e0a641c
by Jürg Billeter at 2018-11-01T11:00:08Z
-
9997c642
by Jürg Billeter at 2018-11-01T11:00:08Z
7 changed files:
- NEWS
- buildstream/_frontend/app.py
- buildstream/buildelement.py
- buildstream/element.py
- buildstream/sandbox/_sandboxremote.py
- buildstream/sandbox/sandbox.py
- tests/frontend/init.py
Changes:
... | ... | @@ -39,6 +39,8 @@ buildstream 1.3.1 |
39 | 39 |
instead of just a specially-formatted build-root with a `root` and `scratch`
|
40 | 40 |
subdirectory.
|
41 | 41 |
|
42 |
+ o Add sandbox API for command batching.
|
|
43 |
+ |
|
42 | 44 |
|
43 | 45 |
=================
|
44 | 46 |
buildstream 1.1.5
|
... | ... | @@ -305,7 +305,6 @@ class App(): |
305 | 305 |
directory = self._main_options['directory']
|
306 | 306 |
directory = os.path.abspath(directory)
|
307 | 307 |
project_path = os.path.join(directory, 'project.conf')
|
308 |
- elements_path = os.path.join(directory, element_path)
|
|
309 | 308 |
|
310 | 309 |
try:
|
311 | 310 |
# Abort if the project.conf already exists, unless `--force` was specified in `bst init`
|
... | ... | @@ -335,6 +334,7 @@ class App(): |
335 | 334 |
raise AppError("Error creating project directory {}: {}".format(directory, e)) from e
|
336 | 335 |
|
337 | 336 |
# Create the elements sub-directory if it doesnt exist
|
337 |
+ elements_path = os.path.join(directory, element_path)
|
|
338 | 338 |
try:
|
339 | 339 |
os.makedirs(elements_path, exist_ok=True)
|
340 | 340 |
except IOError as e:
|
... | ... | @@ -217,7 +217,8 @@ class BuildElement(Element): |
217 | 217 |
# once they are all staged and ready
|
218 | 218 |
with self.timed_activity("Integrating sandbox"):
|
219 | 219 |
for dep in self.dependencies(Scope.BUILD):
|
220 |
- dep.integrate(sandbox)
|
|
220 |
+ dep.integrate(sandbox, queue=True)
|
|
221 |
+ sandbox.run_queue(0)
|
|
221 | 222 |
|
222 | 223 |
# Stage sources in the build root
|
223 | 224 |
self.stage_sources(sandbox, self.get_variable('build-root'))
|
... | ... | @@ -230,9 +231,11 @@ class BuildElement(Element): |
230 | 231 |
if not commands or command_name == 'configure-commands':
|
231 | 232 |
continue
|
232 | 233 |
|
233 |
- with self.timed_activity("Running {}".format(command_name)):
|
|
234 |
+ with sandbox.queue_context_manager(self.timed_activity("Running {}".format(command_name))):
|
|
234 | 235 |
for cmd in commands:
|
235 |
- self.__run_command(sandbox, cmd, command_name)
|
|
236 |
+ self.__queue_command(sandbox, cmd, command_name)
|
|
237 |
+ |
|
238 |
+ sandbox.run_queue(SandboxFlags.ROOT_READ_ONLY)
|
|
236 | 239 |
|
237 | 240 |
# %{install-root}/%{build-root} should normally not be written
|
238 | 241 |
# to - if an element later attempts to stage to a location
|
... | ... | @@ -254,9 +257,9 @@ class BuildElement(Element): |
254 | 257 |
def prepare(self, sandbox):
|
255 | 258 |
commands = self.__commands['configure-commands']
|
256 | 259 |
if commands:
|
257 |
- with self.timed_activity("Running configure-commands"):
|
|
260 |
+ with sandbox.queue_context_manager(self.timed_activity("Running configure-commands")):
|
|
258 | 261 |
for cmd in commands:
|
259 |
- self.__run_command(sandbox, cmd, 'configure-commands')
|
|
262 |
+ self.__queue_command(sandbox, cmd, 'configure-commands')
|
|
260 | 263 |
|
261 | 264 |
def generate_script(self):
|
262 | 265 |
script = ""
|
... | ... | @@ -281,14 +284,18 @@ class BuildElement(Element): |
281 | 284 |
|
282 | 285 |
return commands
|
283 | 286 |
|
284 |
- def __run_command(self, sandbox, cmd, cmd_name):
|
|
285 |
- self.status("Running {}".format(cmd_name), detail=cmd)
|
|
287 |
+ def __queue_command(self, sandbox, cmd, cmd_name):
|
|
288 |
+ def start_cb():
|
|
289 |
+ self.status("Running {}".format(cmd_name), detail=cmd)
|
|
290 |
+ |
|
291 |
+ def complete_cb(exitcode):
|
|
292 |
+ if exitcode != 0:
|
|
293 |
+ raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
|
|
294 |
+ collect=self.get_variable('install-root'))
|
|
286 | 295 |
|
287 | 296 |
# Note the -e switch to 'sh' means to exit with an error
|
288 | 297 |
# if any untested command fails.
|
289 | 298 |
#
|
290 |
- exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'],
|
|
291 |
- SandboxFlags.ROOT_READ_ONLY)
|
|
292 |
- if exitcode != 0:
|
|
293 |
- raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
|
|
294 |
- collect=self.get_variable('install-root'))
|
|
299 |
+ sandbox.queue(['sh', '-c', '-e', cmd + '\n'],
|
|
300 |
+ start_callback=start_cb,
|
|
301 |
+ complete_callback=complete_cb)
|
... | ... | @@ -79,6 +79,7 @@ import copy |
79 | 79 |
from collections import OrderedDict
|
80 | 80 |
from collections.abc import Mapping
|
81 | 81 |
from contextlib import contextmanager
|
82 |
+from functools import partial
|
|
82 | 83 |
import tempfile
|
83 | 84 |
import shutil
|
84 | 85 |
|
... | ... | @@ -753,11 +754,12 @@ class Element(Plugin): |
753 | 754 |
detail += " " + " ".join(["/" + f + "\n" for f in value])
|
754 | 755 |
self.warn("Ignored files", detail=detail)
|
755 | 756 |
|
756 |
- def integrate(self, sandbox):
|
|
757 |
+ def integrate(self, sandbox, *, queue=False):
|
|
757 | 758 |
"""Integrate currently staged filesystem against this artifact.
|
758 | 759 |
|
759 | 760 |
Args:
|
760 | 761 |
sandbox (:class:`.Sandbox`): The build sandbox
|
762 |
+ queue (bool): Whether to queue commands or immediately run them
|
|
761 | 763 |
|
762 | 764 |
This modifies the sysroot staged inside the sandbox so that
|
763 | 765 |
the sysroot is *integrated*. Only an *integrated* sandbox
|
... | ... | @@ -768,14 +770,33 @@ class Element(Plugin): |
768 | 770 |
bstdata = self.get_public_data('bst')
|
769 | 771 |
environment = self.get_environment()
|
770 | 772 |
|
773 |
+ if not queue:
|
|
774 |
+ # Element.integrate() must be called with queue=True if there are
|
|
775 |
+ # commands pending. Otherwise these commands are run implicitly
|
|
776 |
+ # by the call to run_queue() at the end of this method, which is
|
|
777 |
+ # likely not expected by the plugin.
|
|
778 |
+ assert not sandbox._has_commands_queued()
|
|
779 |
+ |
|
771 | 780 |
if bstdata is not None:
|
772 | 781 |
commands = self.node_get_member(bstdata, list, 'integration-commands', [])
|
773 | 782 |
for i in range(len(commands)):
|
774 | 783 |
cmd = self.node_subst_list_element(bstdata, 'integration-commands', [i])
|
775 |
- self.status("Running integration command", detail=cmd)
|
|
776 |
- exitcode = sandbox.run(['sh', '-e', '-c', cmd], 0, env=environment, cwd='/')
|
|
777 |
- if exitcode != 0:
|
|
778 |
- raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
|
|
784 |
+ |
|
785 |
+ def start_cb(cmd):
|
|
786 |
+ self.status("Running integration command", detail=cmd)
|
|
787 |
+ |
|
788 |
+ def complete_cb(cmd, exitcode):
|
|
789 |
+ if exitcode != 0:
|
|
790 |
+ raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
|
|
791 |
+ |
|
792 |
+ sandbox.queue(['sh', '-e', '-c', cmd], env=environment, cwd='/',
|
|
793 |
+ start_callback=partial(start_cb, cmd),
|
|
794 |
+ complete_callback=partial(complete_cb, cmd))
|
|
795 |
+ |
|
796 |
+ if not queue:
|
|
797 |
+ # The caller does not allow keeping pending commands in the queue.
|
|
798 |
+ # Immediately run the pending commands.
|
|
799 |
+ sandbox.run_queue(0)
|
|
779 | 800 |
|
780 | 801 |
def stage_sources(self, sandbox, directory):
|
781 | 802 |
"""Stage this element's sources to a directory in the sandbox
|
... | ... | @@ -2093,7 +2114,15 @@ class Element(Plugin): |
2093 | 2114 |
self.prepare(sandbox)
|
2094 | 2115 |
|
2095 | 2116 |
if workspace:
|
2096 |
- workspace.prepared = True
|
|
2117 |
+ def mark_workspace_prepared():
|
|
2118 |
+ workspace.prepared = True
|
|
2119 |
+ |
|
2120 |
+ if sandbox._has_commands_queued():
|
|
2121 |
+ # Defer workspace.prepared setting until the queued
|
|
2122 |
+ # prepare commands have been executed.
|
|
2123 |
+ sandbox.queue(None, start_callback=mark_workspace_prepared)
|
|
2124 |
+ else:
|
|
2125 |
+ mark_workspace_prepared()
|
|
2097 | 2126 |
|
2098 | 2127 |
def __is_cached(self, keystrength):
|
2099 | 2128 |
if keystrength is None:
|
... | ... | @@ -19,11 +19,14 @@ |
19 | 19 |
# Jim MacArthur <jim macarthur codethink co uk>
|
20 | 20 |
|
21 | 21 |
import os
|
22 |
+import shlex
|
|
23 |
+from collections import deque
|
|
22 | 24 |
from urllib.parse import urlparse
|
23 | 25 |
|
24 | 26 |
import grpc
|
25 | 27 |
|
26 | 28 |
from . import Sandbox
|
29 |
+from .sandbox import _QueueEntryType
|
|
27 | 30 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
28 | 31 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
29 | 32 |
from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
|
... | ... | @@ -189,6 +192,8 @@ class SandboxRemote(Sandbox): |
189 | 192 |
self._set_virtual_directory(new_dir)
|
190 | 193 |
|
191 | 194 |
def run(self, command, flags, *, cwd=None, env=None):
|
195 |
+ stdout, stderr = self._get_output()
|
|
196 |
+ |
|
192 | 197 |
# Upload sources
|
193 | 198 |
upload_vdir = self.get_virtual_directory()
|
194 | 199 |
|
... | ... | @@ -241,13 +246,90 @@ class SandboxRemote(Sandbox): |
241 | 246 |
|
242 | 247 |
action_result = execution_response.result
|
243 | 248 |
|
249 |
+ if stdout:
|
|
250 |
+ if action_result.stdout_raw:
|
|
251 |
+ stdout.write(str(action_result.stdout_raw, 'utf-8', errors='ignore'))
|
|
252 |
+ if stderr:
|
|
253 |
+ if action_result.stderr_raw:
|
|
254 |
+ stderr.write(str(action_result.stderr_raw, 'utf-8', errors='ignore'))
|
|
255 |
+ |
|
244 | 256 |
if action_result.exit_code != 0:
|
245 | 257 |
# A normal error during the build: the remote execution system
|
246 | 258 |
# has worked correctly but the command failed.
|
247 |
- # action_result.stdout and action_result.stderr also contains
|
|
248 |
- # build command outputs which we ignore at the moment.
|
|
249 | 259 |
return action_result.exit_code
|
250 | 260 |
|
251 | 261 |
self.process_job_output(action_result.output_directories, action_result.output_files)
|
252 | 262 |
|
253 | 263 |
return 0
|
264 |
+ |
|
265 |
+ def run_queue(self, flags):
|
|
266 |
+ # If a command fails, all remaining commands in the queue must be discarded.
|
|
267 |
+ queue = self._queue
|
|
268 |
+ self._queue = []
|
|
269 |
+ |
|
270 |
+ script = ""
|
|
271 |
+ i = 0
|
|
272 |
+ first = None
|
|
273 |
+ for entry in queue:
|
|
274 |
+ if entry.type == _QueueEntryType.COMMAND and entry.command:
|
|
275 |
+ if first is None:
|
|
276 |
+ # First command in queue
|
|
277 |
+ first = entry
|
|
278 |
+ else:
|
|
279 |
+ if entry.cwd != cwd:
|
|
280 |
+ script += "mkdir -p {}\n".format(entry.cwd)
|
|
281 |
+ script += "cd {}\n".format(entry.cwd)
|
|
282 |
+ for key in env.keys():
|
|
283 |
+ if key not in entry.env:
|
|
284 |
+ script += "unset {}\n".format(key)
|
|
285 |
+ for key, value in entry.env.items():
|
|
286 |
+ if key not in env or env[key] != value:
|
|
287 |
+ script += "export {}={}\n".format(key, value)
|
|
288 |
+ cwd = entry.cwd
|
|
289 |
+ env = entry.env
|
|
290 |
+ |
|
291 |
+ cmdline = ' '.join(shlex.quote(cmd) for cmd in entry.command)
|
|
292 |
+ script += "({})\n".format(cmdline)
|
|
293 |
+ script += "RETVAL=$?\n"
|
|
294 |
+ script += "if [ $RETVAL -ne 0 ] ; then\n"
|
|
295 |
+ # Report failing command and exit code to stderr (and then back to client)
|
|
296 |
+ script += " echo -e '\nbst-command-failure:' {} $RETVAL >&2\n".format(i)
|
|
297 |
+ script += " exit 1\n"
|
|
298 |
+ script += "fi\n"
|
|
299 |
+ i += 1
|
|
300 |
+ |
|
301 |
+ if first:
|
|
302 |
+ exit_code = self.run(['sh', '-c', script], flags, cwd=first.cwd, env=first.env)
|
|
303 |
+ else:
|
|
304 |
+ exit_code = 0
|
|
305 |
+ |
|
306 |
+ if exit_code != 0:
|
|
307 |
+ # TODO get failed command and exit code from stderr
|
|
308 |
+ failed_command = 0
|
|
309 |
+ command_exit_code = 1
|
|
310 |
+ |
|
311 |
+ i = 0
|
|
312 |
+ # Stack of pending context manager exit methods
|
|
313 |
+ exit_stack = deque()
|
|
314 |
+ try:
|
|
315 |
+ for entry in queue:
|
|
316 |
+ if entry.type == _QueueEntryType.COMMAND:
|
|
317 |
+ entry.start_callback()
|
|
318 |
+ if exit_code == 0 or i < failed_command:
|
|
319 |
+ # Command succeeded
|
|
320 |
+ entry.complete_callback(0)
|
|
321 |
+ else:
|
|
322 |
+ # Command failed
|
|
323 |
+ entry.complete_callback(command_exit_code)
|
|
324 |
+ break
|
|
325 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
|
|
326 |
+ entry.cm.__enter__()
|
|
327 |
+ exit_stack.append(entry.cm.__exit__)
|
|
328 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
|
|
329 |
+ exit_cb = exit_stack.pop()
|
|
330 |
+ exit_cb(None, None, None)
|
|
331 |
+ i += 1
|
|
332 |
+ finally:
|
|
333 |
+ while exit_stack:
|
|
334 |
+ exit_cb = exit_stack.pop()
|
|
335 |
+ exit_cb(None, None, None)
|
... | ... | @@ -29,6 +29,9 @@ See also: :ref:`sandboxing`. |
29 | 29 |
"""
|
30 | 30 |
|
31 | 31 |
import os
|
32 |
+from collections import deque, namedtuple
|
|
33 |
+from contextlib import contextmanager
|
|
34 |
+ |
|
32 | 35 |
from .._exceptions import ImplError, BstError
|
33 | 36 |
from ..storage._filebaseddirectory import FileBasedDirectory
|
34 | 37 |
from ..storage._casbaseddirectory import CasBasedDirectory
|
... | ... | @@ -71,6 +74,22 @@ class SandboxFlags(): |
71 | 74 |
"""
|
72 | 75 |
|
73 | 76 |
|
77 |
+# _QueueEntryType()
|
|
78 |
+#
|
|
79 |
+# A type for sandbox queue entries.
|
|
80 |
+#
|
|
81 |
+class _QueueEntryType():
|
|
82 |
+ |
|
83 |
+ # A queued command.
|
|
84 |
+ COMMAND = 1
|
|
85 |
+ |
|
86 |
+ # A queued context manager enter.
|
|
87 |
+ CONTEXT_MANAGER_ENTER = 2
|
|
88 |
+ |
|
89 |
+ # A queued context manager exit.
|
|
90 |
+ CONTEXT_MANAGER_EXIT = 3
|
|
91 |
+ |
|
92 |
+ |
|
74 | 93 |
class Sandbox():
|
75 | 94 |
"""Sandbox()
|
76 | 95 |
|
... | ... | @@ -121,6 +140,9 @@ class Sandbox(): |
121 | 140 |
# directory via get_directory.
|
122 | 141 |
self._never_cache_vdirs = False
|
123 | 142 |
|
143 |
+ # Queued commands
|
|
144 |
+ self._queue = []
|
|
145 |
+ |
|
124 | 146 |
def get_directory(self):
|
125 | 147 |
"""Fetches the sandbox root directory
|
126 | 148 |
|
... | ... | @@ -237,6 +259,97 @@ class Sandbox(): |
237 | 259 |
raise ImplError("Sandbox of type '{}' does not implement run()"
|
238 | 260 |
.format(type(self).__name__))
|
239 | 261 |
|
262 |
+ def queue(self, command, *, cwd=None, env=None, start_callback=None, complete_callback=None):
|
|
263 |
+ """Queue a command to be run in the sandbox.
|
|
264 |
+ |
|
265 |
+ If the command fails, commands queued later will not be executed.
|
|
266 |
+ The callbacks are not guaranteed to be invoked in real time.
|
|
267 |
+ |
|
268 |
+ Args:
|
|
269 |
+ command (list): The command to run in the sandboxed environment, as a list
|
|
270 |
+ of strings starting with the binary to run.
|
|
271 |
+ cwd (str): The sandbox relative working directory in which to run the command.
|
|
272 |
+ env (dict): A dictionary of string key, value pairs to set as environment
|
|
273 |
+ variables inside the sandbox environment.
|
|
274 |
+ start_callback (callable): Called when the command starts.
|
|
275 |
+ complete_callback (callble): Called when the command completes
|
|
276 |
+ with the exit code as argument.
|
|
277 |
+ |
|
278 |
+ .. note::
|
|
279 |
+ |
|
280 |
+ The optional *cwd* argument will default to the value set with
|
|
281 |
+ :func:`~buildstream.sandbox.Sandbox.set_work_directory`
|
|
282 |
+ |
|
283 |
+ *Since: 1.4*
|
|
284 |
+ """
|
|
285 |
+ entry = namedtuple('QueueEntry', ['type', 'command', 'cwd', 'env', 'start_callback', 'complete_callback'])
|
|
286 |
+ entry.type = _QueueEntryType.COMMAND
|
|
287 |
+ entry.command = command
|
|
288 |
+ entry.cwd = self._get_work_directory(cwd=cwd)
|
|
289 |
+ entry.env = self._get_environment(cwd=cwd, env=env)
|
|
290 |
+ entry.start_callback = start_callback
|
|
291 |
+ entry.complete_callback = complete_callback
|
|
292 |
+ self._queue.append(entry)
|
|
293 |
+ |
|
294 |
+ @contextmanager
|
|
295 |
+ def queue_context_manager(self, cm):
|
|
296 |
+ """Queue entering and exiting a context manager
|
|
297 |
+ |
|
298 |
+ Args:
|
|
299 |
+ cm: A context manager
|
|
300 |
+ |
|
301 |
+ *Since: 1.4*
|
|
302 |
+ """
|
|
303 |
+ entry = namedtuple('QueueEntry', ['type', 'cm'])
|
|
304 |
+ entry.type = _QueueEntryType.CONTEXT_MANAGER_ENTER
|
|
305 |
+ entry.cm = cm
|
|
306 |
+ self._queue.append(entry)
|
|
307 |
+ yield
|
|
308 |
+ entry = namedtuple('QueueEntry', ['type'])
|
|
309 |
+ entry.type = _QueueEntryType.CONTEXT_MANAGER_EXIT
|
|
310 |
+ self._queue.append(entry)
|
|
311 |
+ |
|
312 |
+ def run_queue(self, flags):
|
|
313 |
+ """Run a command in the sandbox.
|
|
314 |
+ |
|
315 |
+ Args:
|
|
316 |
+ flags (:class:`.SandboxFlags`): The flags for running this command.
|
|
317 |
+ |
|
318 |
+ Raises:
|
|
319 |
+ (:class:`.ProgramNotFoundError`): If a host tool which the given sandbox
|
|
320 |
+ implementation requires is not found.
|
|
321 |
+ |
|
322 |
+ *Since: 1.4*
|
|
323 |
+ """
|
|
324 |
+ # If a command fails, all remaining commands in the queue must be discarded.
|
|
325 |
+ queue = self._queue
|
|
326 |
+ self._queue = []
|
|
327 |
+ |
|
328 |
+ # Stack of pending context manager exit methods
|
|
329 |
+ exit_stack = deque()
|
|
330 |
+ try:
|
|
331 |
+ for entry in queue:
|
|
332 |
+ if entry.type == _QueueEntryType.COMMAND:
|
|
333 |
+ if entry.start_callback:
|
|
334 |
+ entry.start_callback()
|
|
335 |
+ if entry.command:
|
|
336 |
+ # pylint: disable=assignment-from-no-return
|
|
337 |
+ exit_code = self.run(entry.command, flags, cwd=entry.cwd, env=entry.env)
|
|
338 |
+ if entry.complete_callback:
|
|
339 |
+ entry.complete_callback(exit_code)
|
|
340 |
+ if exit_code != 0:
|
|
341 |
+ break
|
|
342 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
|
|
343 |
+ entry.cm.__enter__()
|
|
344 |
+ exit_stack.append(entry.cm.__exit__)
|
|
345 |
+ elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
|
|
346 |
+ exit_cb = exit_stack.pop()
|
|
347 |
+ exit_cb(None, None, None)
|
|
348 |
+ finally:
|
|
349 |
+ while exit_stack:
|
|
350 |
+ exit_cb = exit_stack.pop()
|
|
351 |
+ exit_cb(None, None, None)
|
|
352 |
+ |
|
240 | 353 |
################################################
|
241 | 354 |
# Private methods #
|
242 | 355 |
################################################
|
... | ... | @@ -385,3 +498,12 @@ class Sandbox(): |
385 | 498 |
return True
|
386 | 499 |
|
387 | 500 |
return False
|
501 |
+ |
|
502 |
+ # _has_commands_queued()
|
|
503 |
+ #
|
|
504 |
+ # Returns whether the sandbox has a non-empty queue of pending commands.
|
|
505 |
+ #
|
|
506 |
+ # Returns:
|
|
507 |
+ # (bool): Whether the command queue is non-empty.
|
|
508 |
+ def _has_commands_queued(self):
|
|
509 |
+ return len(self._queue) > 0
|
... | ... | @@ -3,6 +3,7 @@ import pytest |
3 | 3 |
from tests.testutils import cli
|
4 | 4 |
|
5 | 5 |
from buildstream import _yaml
|
6 |
+from buildstream._frontend.app import App
|
|
6 | 7 |
from buildstream._exceptions import ErrorDomain, LoadErrorReason
|
7 | 8 |
from buildstream._versions import BST_FORMAT_VERSION
|
8 | 9 |
|
... | ... | @@ -98,3 +99,34 @@ def test_bad_element_path(cli, tmpdir, element_path): |
98 | 99 |
'init', '--project-name', 'foo', '--element-path', element_path
|
99 | 100 |
])
|
100 | 101 |
result.assert_main_error(ErrorDomain.APP, 'invalid-element-path')
|
102 |
+ |
|
103 |
+ |
|
104 |
+@pytest.mark.parametrize("element_path", [('foo'), ('foo/bar')])
|
|
105 |
+def test_element_path_interactive(cli, tmp_path, monkeypatch, element_path):
|
|
106 |
+ project = tmp_path
|
|
107 |
+ project_conf_path = project.joinpath('project.conf')
|
|
108 |
+ |
|
109 |
+ class DummyInteractiveApp(App):
|
|
110 |
+ def __init__(self, *args, **kwargs):
|
|
111 |
+ super().__init__(*args, **kwargs)
|
|
112 |
+ self.interactive = True
|
|
113 |
+ |
|
114 |
+ @classmethod
|
|
115 |
+ def create(cls, *args, **kwargs):
|
|
116 |
+ return DummyInteractiveApp(*args, **kwargs)
|
|
117 |
+ |
|
118 |
+ def _init_project_interactive(self, *args, **kwargs):
|
|
119 |
+ return ('project_name', '0', element_path)
|
|
120 |
+ |
|
121 |
+ monkeypatch.setattr(App, 'create', DummyInteractiveApp.create)
|
|
122 |
+ |
|
123 |
+ result = cli.run(project=str(project), args=['init'])
|
|
124 |
+ result.assert_success()
|
|
125 |
+ |
|
126 |
+ full_element_path = project.joinpath(element_path)
|
|
127 |
+ assert full_element_path.exists()
|
|
128 |
+ |
|
129 |
+ project_conf = _yaml.load(str(project_conf_path))
|
|
130 |
+ assert project_conf['name'] == 'project_name'
|
|
131 |
+ assert project_conf['format-version'] == '0'
|
|
132 |
+ assert project_conf['element-path'] == element_path
|