summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbst-marge-bot <marge-bot@buildstream.build>2019-03-21 14:55:45 +0000
committerbst-marge-bot <marge-bot@buildstream.build>2019-03-21 14:55:45 +0000
commitbc1b971e169f2fc721c61276c071640caee6a3f8 (patch)
treecd03efcc726e07eb0c33c49d4385b9888c4af803
parentc74701413ed32d721188b2a7eab8081d4629380c (diff)
parent65abe0fcc103478ccfeeb38d57ca38be02a15fe6 (diff)
downloadbuildstream-bc1b971e169f2fc721c61276c071640caee6a3f8.tar.gz
Merge branch 'bschubert/pipeline' into 'master'
Refactor _update_state() to be called only when needed Closes #703 See merge request BuildStream/buildstream!1070
-rw-r--r--buildstream/_context.py4
-rw-r--r--buildstream/_frontend/app.py2
-rw-r--r--buildstream/_frontend/widget.py8
-rw-r--r--buildstream/_scheduler/jobs/elementjob.py4
-rw-r--r--buildstream/_scheduler/queues/buildqueue.py3
-rw-r--r--buildstream/_scheduler/queues/fetchqueue.py5
-rw-r--r--buildstream/_scheduler/queues/pullqueue.py3
-rw-r--r--buildstream/_scheduler/queues/queue.py2
-rw-r--r--buildstream/_scheduler/queues/trackqueue.py4
-rw-r--r--buildstream/_stream.py13
-rw-r--r--buildstream/element.py46
-rw-r--r--buildstream/plugin.py100
-rw-r--r--buildstream/sandbox/sandbox.py2
-rw-r--r--buildstream/types.py51
-rw-r--r--tests/frontend/workspace.py55
-rw-r--r--tests/frontend/workspaced-build-dep/elements/elem1.bst5
-rw-r--r--tests/frontend/workspaced-build-dep/elements/elem2.bst8
-rw-r--r--tests/frontend/workspaced-build-dep/elements/elem3.bst8
-rw-r--r--tests/frontend/workspaced-build-dep/elements/elem4.bst8
-rw-r--r--tests/frontend/workspaced-build-dep/elements/elem5.bst8
-rw-r--r--tests/frontend/workspaced-build-dep/elements/stack.bst4
-rw-r--r--tests/frontend/workspaced-build-dep/files/file10
-rw-r--r--tests/frontend/workspaced-build-dep/files/file20
-rw-r--r--tests/frontend/workspaced-build-dep/files/file30
-rw-r--r--tests/frontend/workspaced-build-dep/files/file40
-rw-r--r--tests/frontend/workspaced-build-dep/project.conf8
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/elem1.bst5
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/elem2.bst8
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/elem3.bst8
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/elem4.bst8
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/elem5.bst8
-rw-r--r--tests/frontend/workspaced-runtime-dep/elements/stack.bst4
-rw-r--r--tests/frontend/workspaced-runtime-dep/files/file10
-rw-r--r--tests/frontend/workspaced-runtime-dep/files/file20
-rw-r--r--tests/frontend/workspaced-runtime-dep/files/file30
-rw-r--r--tests/frontend/workspaced-runtime-dep/files/file40
-rw-r--r--tests/frontend/workspaced-runtime-dep/project.conf8
37 files changed, 311 insertions, 89 deletions
diff --git a/buildstream/_context.py b/buildstream/_context.py
index 476032f39..0c7f81264 100644
--- a/buildstream/_context.py
+++ b/buildstream/_context.py
@@ -35,7 +35,7 @@ from ._artifactcache import ArtifactCache
from ._sourcecache import SourceCache
from ._cas import CASCache, CASQuota, CASCacheUsage
from ._workspaces import Workspaces, WorkspaceProjectCache
-from .plugin import _plugin_lookup
+from .plugin import Plugin
from .sandbox import SandboxRemote
@@ -651,7 +651,7 @@ class Context():
plugin_name = ""
if message.unique_id:
template += " {plugin}"
- plugin = _plugin_lookup(message.unique_id)
+ plugin = Plugin._lookup(message.unique_id)
plugin_name = plugin.name
template += ": {message}"
diff --git a/buildstream/_frontend/app.py b/buildstream/_frontend/app.py
index 67fcc8370..a659b202a 100644
--- a/buildstream/_frontend/app.py
+++ b/buildstream/_frontend/app.py
@@ -531,7 +531,7 @@ class App():
queue = job.queue
# Get the last failure message for additional context
- failure = self._fail_messages.get(element._get_unique_id())
+ failure = self._fail_messages.get(element._unique_id)
# XXX This is dangerous, sometimes we get the job completed *before*
# the failure message reaches us ??
diff --git a/buildstream/_frontend/widget.py b/buildstream/_frontend/widget.py
index 45be6d136..ef31b8ba7 100644
--- a/buildstream/_frontend/widget.py
+++ b/buildstream/_frontend/widget.py
@@ -32,7 +32,7 @@ from .. import _yaml
from .. import __version__ as bst_version
from .._exceptions import ImplError
from .._message import MessageType
-from ..plugin import _plugin_lookup
+from ..plugin import Plugin
# These messages are printed a bit differently
@@ -191,7 +191,7 @@ class ElementName(Widget):
action_name = message.action_name
element_id = message.task_id or message.unique_id
if element_id is not None:
- plugin = _plugin_lookup(element_id)
+ plugin = Plugin._lookup(element_id)
name = plugin._get_full_name()
name = '{: <30}'.format(name)
else:
@@ -232,7 +232,7 @@ class CacheKey(Widget):
missing = False
key = ' ' * self._key_length
- plugin = _plugin_lookup(element_id)
+ plugin = Plugin._lookup(element_id)
if isinstance(plugin, Element):
_, key, missing = plugin._get_display_key()
@@ -621,7 +621,7 @@ class LogLine(Widget):
# Track logfiles for later use
element_id = message.task_id or message.unique_id
if message.message_type in ERROR_MESSAGES and element_id is not None:
- plugin = _plugin_lookup(element_id)
+ plugin = Plugin._lookup(element_id)
self._failure_messages[plugin].append(message)
return self._render(message)
diff --git a/buildstream/_scheduler/jobs/elementjob.py b/buildstream/_scheduler/jobs/elementjob.py
index fa0d34fb3..fb5d38e11 100644
--- a/buildstream/_scheduler/jobs/elementjob.py
+++ b/buildstream/_scheduler/jobs/elementjob.py
@@ -73,7 +73,7 @@ class ElementJob(Job):
self._complete_cb = complete_cb # The complete callable function
# Set the task wide ID for logging purposes
- self.set_task_id(element._get_unique_id())
+ self.set_task_id(element._unique_id)
@property
def element(self):
@@ -100,7 +100,7 @@ class ElementJob(Job):
args = dict(kwargs)
args['scheduler'] = True
self._scheduler.context.message(
- Message(self._element._get_unique_id(),
+ Message(self._element._unique_id,
message_type,
message,
**args))
diff --git a/buildstream/_scheduler/queues/buildqueue.py b/buildstream/_scheduler/queues/buildqueue.py
index 60ec19ff4..aa489f381 100644
--- a/buildstream/_scheduler/queues/buildqueue.py
+++ b/buildstream/_scheduler/queues/buildqueue.py
@@ -70,9 +70,6 @@ class BuildQueue(Queue):
return element._assemble()
def status(self, element):
- # state of dependencies may have changed, recalculate element state
- element._update_state()
-
if not element._is_required():
# Artifact is not currently required but it may be requested later.
# Keep it in the queue.
diff --git a/buildstream/_scheduler/queues/fetchqueue.py b/buildstream/_scheduler/queues/fetchqueue.py
index db5e470f9..546c65b65 100644
--- a/buildstream/_scheduler/queues/fetchqueue.py
+++ b/buildstream/_scheduler/queues/fetchqueue.py
@@ -45,9 +45,6 @@ class FetchQueue(Queue):
element._fetch(fetch_original=self._fetch_original)
def status(self, element):
- # state of dependencies may have changed, recalculate element state
- element._update_state()
-
if not element._is_required():
# Artifact is not currently required but it may be requested later.
# Keep it in the queue.
@@ -74,7 +71,7 @@ class FetchQueue(Queue):
if status == JobStatus.FAIL:
return
- element._update_state()
+ element._fetch_done()
# Successful fetch, we must be CACHED now
assert element._get_consistency() == Consistency.CACHED
diff --git a/buildstream/_scheduler/queues/pullqueue.py b/buildstream/_scheduler/queues/pullqueue.py
index dbeb806e5..013ee6489 100644
--- a/buildstream/_scheduler/queues/pullqueue.py
+++ b/buildstream/_scheduler/queues/pullqueue.py
@@ -39,9 +39,6 @@ class PullQueue(Queue):
raise SkipJob(self.action_name)
def status(self, element):
- # state of dependencies may have changed, recalculate element state
- element._update_state()
-
if not element._is_required():
# Artifact is not currently required but it may be requested later.
# Keep it in the queue.
diff --git a/buildstream/_scheduler/queues/queue.py b/buildstream/_scheduler/queues/queue.py
index 9f0563fa5..1efcffc16 100644
--- a/buildstream/_scheduler/queues/queue.py
+++ b/buildstream/_scheduler/queues/queue.py
@@ -316,7 +316,7 @@ class Queue():
# a message for the element they are processing
def _message(self, element, message_type, brief, **kwargs):
context = element._get_context()
- message = Message(element._get_unique_id(), message_type, brief, **kwargs)
+ message = Message(element._unique_id, message_type, brief, **kwargs)
context.message(message)
def _element_log_path(self, element):
diff --git a/buildstream/_scheduler/queues/trackqueue.py b/buildstream/_scheduler/queues/trackqueue.py
index 245b528e6..d7e6546f3 100644
--- a/buildstream/_scheduler/queues/trackqueue.py
+++ b/buildstream/_scheduler/queues/trackqueue.py
@@ -19,7 +19,7 @@
# Jürg Billeter <juerg.billeter@codethink.co.uk>
# BuildStream toplevel imports
-from ...plugin import _plugin_lookup
+from ...plugin import Plugin
# Local imports
from . import Queue, QueueStatus
@@ -55,7 +55,7 @@ class TrackQueue(Queue):
# Set the new refs in the main process one by one as they complete
for unique_id, new_ref in result:
- source = _plugin_lookup(unique_id)
+ source = Plugin._lookup(unique_id)
source._save_ref(new_ref)
element._tracking_done()
diff --git a/buildstream/_stream.py b/buildstream/_stream.py
index 3276b4081..262b38852 100644
--- a/buildstream/_stream.py
+++ b/buildstream/_stream.py
@@ -32,7 +32,7 @@ from contextlib import contextmanager, suppress
from fnmatch import fnmatch
from ._artifactelement import verify_artifact_ref
-from ._exceptions import StreamError, ImplError, BstError, ArtifactElementError, CASCacheError, set_last_task_error
+from ._exceptions import StreamError, ImplError, BstError, ArtifactElementError, CASCacheError
from ._message import Message, MessageType
from ._scheduler import Scheduler, SchedStatus, TrackQueue, FetchQueue, BuildQueue, PullQueue, PushQueue
from ._pipeline import Pipeline, PipelineSelection
@@ -1172,17 +1172,6 @@ class Stream():
_, status = self._scheduler.run(self.queues)
- # Force update element states after a run, such that the summary
- # is more coherent
- try:
- for element in self.total_elements:
- element._update_state()
- except BstError as e:
- self._message(MessageType.ERROR, "Error resolving final state", detail=str(e))
- set_last_task_error(e.domain, e.reason)
- except Exception as e: # pylint: disable=broad-except
- self._message(MessageType.BUG, "Unhandled exception while resolving final state", detail=str(e))
-
if status == SchedStatus.ERROR:
raise StreamError()
elif status == SchedStatus.TERMINATED:
diff --git a/buildstream/element.py b/buildstream/element.py
index 35f656fca..88f6863d6 100644
--- a/buildstream/element.py
+++ b/buildstream/element.py
@@ -99,7 +99,7 @@ from . import _site
from ._platform import Platform
from .sandbox._config import SandboxConfig
from .sandbox._sandboxremote import SandboxRemote
-from .types import _KeyStrength, CoreWarnings
+from .types import _KeyStrength, CoreWarnings, _UniquePriorityQueue
from ._artifact import Artifact
from .storage.directory import Directory
@@ -204,6 +204,8 @@ class Element(Plugin):
self.__runtime_dependencies = [] # Direct runtime dependency Elements
self.__build_dependencies = [] # Direct build dependency Elements
+ self.__reverse_dependencies = set() # Direct reverse dependency Elements
+ self.__ready_for_runtime = False # Wether the element has all its dependencies ready and has a cache key
self.__sources = [] # List of Sources
self.__weak_cache_key = None # Our cached weak cache key
self.__strict_cache_key = None # Our cached cache key for strict builds
@@ -975,9 +977,12 @@ class Element(Plugin):
for meta_dep in meta.dependencies:
dependency = Element._new_from_meta(meta_dep)
element.__runtime_dependencies.append(dependency)
+ dependency.__reverse_dependencies.add(element)
+
for meta_dep in meta.build_dependencies:
dependency = Element._new_from_meta(meta_dep)
element.__build_dependencies.append(dependency)
+ dependency.__reverse_dependencies.add(element)
return element
@@ -1259,6 +1264,10 @@ class Element(Plugin):
# Strong cache key could not be calculated yet
return
+ if not self.__ready_for_runtime and self.__cache_key is not None:
+ self.__ready_for_runtime = all(
+ dep.__ready_for_runtime for dep in self.__runtime_dependencies)
+
# _get_display_key():
#
# Returns cache keys for display purposes
@@ -1373,7 +1382,7 @@ class Element(Plugin):
source = sources.pop()
source._generate_key(sources)
- self._update_state()
+ self.__update_state_recursively()
# _track():
#
@@ -1390,7 +1399,7 @@ class Element(Plugin):
for index, source in enumerate(self.__sources):
old_ref = source.get_ref()
new_ref = source._track(self.__sources[0:index])
- refs.append((source._get_unique_id(), new_ref))
+ refs.append((source._unique_id, new_ref))
# Complimentary warning that the new ref will be unused.
if old_ref != new_ref and self._get_workspace():
@@ -1580,7 +1589,7 @@ class Element(Plugin):
self.__assemble_scheduled = False
self.__assemble_done = True
- self._update_state()
+ self.__update_state_recursively()
if self._get_workspace() and self._cached_success():
assert utils._is_main_process(), \
@@ -1765,6 +1774,15 @@ class Element(Plugin):
def _get_build_log(self):
return self._build_log_path
+ # _fetch_done()
+ #
+ # Indicates that fetching the sources for this element has been done.
+ #
+ def _fetch_done(self):
+ # We are not updating the state recursively here since fetching can
+ # never end up in updating them.
+ self._update_state()
+
# _pull_pending()
#
# Check whether the artifact will be pulled. If the pull operation is to
@@ -1807,7 +1825,7 @@ class Element(Plugin):
def _pull_done(self):
self.__pull_done = True
- self._update_state()
+ self.__update_state_recursively()
# _pull():
#
@@ -2915,6 +2933,24 @@ class Element(Plugin):
if not sourcecache.contains(sources[-1]):
sources[-1]._cache(sources[:-1])
+ # __update_state_recursively()
+ #
+ # Update the state of all reverse dependencies, recursively.
+ #
+ def __update_state_recursively(self):
+ queue = _UniquePriorityQueue()
+ queue.push(self._unique_id, self)
+
+ while queue:
+ element = queue.pop()
+
+ old_ready_for_runtime = element.__ready_for_runtime
+ element._update_state()
+
+ if element.__ready_for_runtime != old_ready_for_runtime:
+ for rdep in element.__reverse_dependencies:
+ queue.push(rdep._unique_id, rdep)
+
def _overlap_error_detail(f, forbidden_overlap_elements, elements):
if forbidden_overlap_elements:
diff --git a/buildstream/plugin.py b/buildstream/plugin.py
index f5daf31bf..c139c8cb7 100644
--- a/buildstream/plugin.py
+++ b/buildstream/plugin.py
@@ -109,6 +109,7 @@ Class Reference
---------------
"""
+import itertools
import os
import subprocess
import sys
@@ -183,6 +184,23 @@ class Plugin():
"""
+ # Unique id generator for Plugins
+ #
+ # Each plugin gets a unique id at creation.
+ # Ids are a monotically increasing integer
+ __id_generator = itertools.count()
+
+ # Hold on to a lookup table by counter of all instantiated plugins.
+ # We use this to send the id back from child processes so we can lookup
+ # corresponding element/source in the master process.
+ #
+ # Use WeakValueDictionary() so the map we use to lookup objects does not
+ # keep the plugins alive after pipeline destruction.
+ #
+ # Note that Plugins can only be instantiated in the main process before
+ # scheduling tasks.
+ __TABLE = WeakValueDictionary()
+
def __init__(self, name, context, project, provenance, type_tag):
self.name = name
@@ -195,11 +213,24 @@ class Plugin():
For sources this is for display purposes only.
"""
+ # Unique ID
+ #
+ # This id allows to uniquely identify a plugin.
+ #
+ # /!\ the unique id must be an increasing value /!\
+ # This is because we are depending on it in buildstream.element.Element
+ # to give us a topological sort over all elements.
+ # Modifying how we handle ids here will modify the behavior of the
+ # Element's state handling.
+ self._unique_id = next(self.__id_generator)
+
+ # register ourself in the table containing all existing plugins
+ self.__TABLE[self._unique_id] = self
+
self.__context = context # The Context object
self.__project = project # The Project object
self.__provenance = provenance # The Provenance information
self.__type_tag = type_tag # The type of plugin (element or source)
- self.__unique_id = _plugin_register(self) # Unique ID
self.__configuring = False # Whether we are currently configuring
# Infer the kind identifier
@@ -577,7 +608,7 @@ class Plugin():
self.call(... command which takes time ...)
"""
with self.__context.timed_activity(activity_name,
- unique_id=self.__unique_id,
+ unique_id=self._unique_id,
detail=detail,
silent_nested=silent_nested):
yield
@@ -669,6 +700,23 @@ class Plugin():
# Private Methods used in BuildStream #
#############################################################
+ # _lookup():
+ #
+ # Fetch a plugin in the current process by its
+ # unique identifier
+ #
+ # Args:
+ # unique_id: The unique identifier as returned by
+ # plugin._unique_id
+ #
+ # Returns:
+ # (Plugin): The plugin for the given ID, or None
+ #
+ @classmethod
+ def _lookup(cls, unique_id):
+ assert unique_id in cls.__TABLE, "Could not find plugin with ID {}".format(unique_id)
+ return cls.__TABLE[unique_id]
+
# _get_context()
#
# Fetches the invocation context
@@ -683,13 +731,6 @@ class Plugin():
def _get_project(self):
return self.__project
- # _get_unique_id():
- #
- # Fetch the plugin's unique identifier
- #
- def _get_unique_id(self):
- return self.__unique_id
-
# _get_provenance():
#
# Fetch bst file, line and column of the entity
@@ -774,7 +815,7 @@ class Plugin():
return (exit_code, output)
def __message(self, message_type, brief, **kwargs):
- message = Message(self.__unique_id, message_type, brief, **kwargs)
+ message = Message(self._unique_id, message_type, brief, **kwargs)
self.__context.message(message)
def __note_command(self, output, *popenargs, **kwargs):
@@ -806,45 +847,6 @@ class Plugin():
return self.get_kind() in silenced_warnings
-# Hold on to a lookup table by counter of all instantiated plugins.
-# We use this to send the id back from child processes so we can lookup
-# corresponding element/source in the master process.
-#
-# Use WeakValueDictionary() so the map we use to lookup objects does not
-# keep the plugins alive after pipeline destruction.
-#
-# Note that Plugins can only be instantiated in the main process before
-# scheduling tasks.
-__PLUGINS_UNIQUE_ID = 0
-__PLUGINS_TABLE = WeakValueDictionary()
-
-
-# _plugin_lookup():
-#
-# Fetch a plugin in the current process by its
-# unique identifier
-#
-# Args:
-# unique_id: The unique identifier as returned by
-# plugin._get_unique_id()
-#
-# Returns:
-# (Plugin): The plugin for the given ID, or None
-#
-def _plugin_lookup(unique_id):
- assert unique_id in __PLUGINS_TABLE, "Could not find plugin with ID {}".format(unique_id)
- return __PLUGINS_TABLE[unique_id]
-
-
-# No need for unregister, WeakValueDictionary() will remove entries
-# in itself when the referenced plugins are garbage collected.
-def _plugin_register(plugin):
- global __PLUGINS_UNIQUE_ID # pylint: disable=global-statement
- __PLUGINS_UNIQUE_ID += 1
- __PLUGINS_TABLE[__PLUGINS_UNIQUE_ID] = plugin
- return __PLUGINS_UNIQUE_ID
-
-
# A local table for _prefix_warning()
#
__CORE_WARNINGS = [
diff --git a/buildstream/sandbox/sandbox.py b/buildstream/sandbox/sandbox.py
index f11ddea1d..93db2f8ca 100644
--- a/buildstream/sandbox/sandbox.py
+++ b/buildstream/sandbox/sandbox.py
@@ -122,7 +122,7 @@ class Sandbox():
# Plugin ID for logging
plugin = kwargs.get('plugin', None)
if plugin:
- self.__plugin_id = plugin._get_unique_id()
+ self.__plugin_id = plugin._unique_id
else:
self.__plugin_id = None
diff --git a/buildstream/types.py b/buildstream/types.py
index ba4b99eb7..d54bf0b6e 100644
--- a/buildstream/types.py
+++ b/buildstream/types.py
@@ -17,6 +17,7 @@
# Authors:
# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
# Jim MacArthur <jim.macarthur@codethink.co.uk>
+# Benjamin Schubert <bschubert15@bloomberg.net>
"""
Foundation types
@@ -25,6 +26,7 @@ Foundation types
"""
from enum import Enum
+import heapq
class Scope(Enum):
@@ -124,3 +126,52 @@ class _KeyStrength(Enum):
# Includes names of direct build dependencies but does not include
# cache keys of dependencies.
WEAK = 2
+
+
+# _UniquePriorityQueue():
+#
+# Implements a priority queue that adds only each key once.
+#
+# The queue will store and priority based on a tuple (key, item).
+#
+class _UniquePriorityQueue:
+
+ def __init__(self):
+ self._items = set()
+ self._heap = []
+
+ # push():
+ #
+ # Push a new item in the queue.
+ #
+ # If the item is already present in the queue as identified by the key,
+ # this is a noop.
+ #
+ # Args:
+ # key (hashable, comparable): unique key to use for checking for
+ # the object's existence and used for
+ # ordering
+ # item (any): item to push to the queue
+ #
+ def push(self, key, item):
+ if key not in self._items:
+ self._items.add(key)
+ heapq.heappush(self._heap, (key, item))
+
+ # pop():
+ #
+ # Pop the next item from the queue, by priority order.
+ #
+ # Returns:
+ # (any): the next item
+ #
+ # Throw:
+ # IndexError: when the list is empty
+ #
+ def pop(self):
+ key, item = heapq.heappop(self._heap)
+ self._items.remove(key)
+ return item
+
+ def __len__(self):
+ return len(self._heap)
diff --git a/tests/frontend/workspace.py b/tests/frontend/workspace.py
index c47424ec6..e570c2899 100644
--- a/tests/frontend/workspace.py
+++ b/tests/frontend/workspace.py
@@ -1229,3 +1229,58 @@ def test_external_list(cli, datafiles, tmpdir_factory):
result = cli.run(project=project, args=['-C', workspace, 'workspace', 'list'])
result.assert_success()
+
+
+# This strange test tests against a regression raised in issue #919,
+# where opening a workspace on a runtime dependency of a build only
+# dependency causes `bst build` to not build the specified target
+# but just successfully builds the workspaced element and happily
+# exits without completing the build.
+#
+TEST_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__))
+)
+
+
+@pytest.mark.datafiles(TEST_DIR)
+@pytest.mark.parametrize(
+ ["case", "non_workspaced_elements_state"],
+ [
+ ("workspaced-build-dep", ["waiting", "waiting", "waiting", "waiting", "waiting"]),
+ ("workspaced-runtime-dep", ["buildable", "buildable", "waiting", "waiting", "waiting"])
+ ],
+)
+@pytest.mark.parametrize("strict", [("strict"), ("non-strict")])
+def test_build_all(cli, tmpdir, datafiles, case, strict, non_workspaced_elements_state):
+ project = os.path.join(str(datafiles), case)
+ workspace = os.path.join(str(tmpdir), 'workspace')
+ non_leaf_elements = ["elem2.bst", "elem3.bst", "stack.bst", "elem4.bst", "elem5.bst"]
+ all_elements = ["elem1.bst", *non_leaf_elements]
+
+ # Configure strict mode
+ strict_mode = True
+ if strict != 'strict':
+ strict_mode = False
+ cli.configure({
+ 'projects': {
+ 'test': {
+ 'strict': strict_mode
+ }
+ }
+ })
+
+ # First open the workspace
+ result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, 'elem1.bst'])
+ result.assert_success()
+
+ # Ensure all elements are waiting build the first
+ assert cli.get_element_states(project, all_elements) == \
+ dict(zip(all_elements, ['buildable', *non_workspaced_elements_state]))
+
+ # Now build the targets elem4.bst and elem5.bst
+ result = cli.run(project=project, args=['build', 'elem4.bst', 'elem5.bst'])
+ result.assert_success()
+
+ # Assert that the target is built
+ assert cli.get_element_states(project, all_elements) == \
+ {elem: "cached" for elem in all_elements}
diff --git a/tests/frontend/workspaced-build-dep/elements/elem1.bst b/tests/frontend/workspaced-build-dep/elements/elem1.bst
new file mode 100644
index 000000000..eed39a95a
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/elem1.bst
@@ -0,0 +1,5 @@
+kind: import
+
+sources:
+- kind: local
+ path: files/file1
diff --git a/tests/frontend/workspaced-build-dep/elements/elem2.bst b/tests/frontend/workspaced-build-dep/elements/elem2.bst
new file mode 100644
index 000000000..78af3ba44
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/elem2.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- elem1.bst
+
+sources:
+- kind: local
+ path: files/file2
diff --git a/tests/frontend/workspaced-build-dep/elements/elem3.bst b/tests/frontend/workspaced-build-dep/elements/elem3.bst
new file mode 100644
index 000000000..6e1de0de4
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/elem3.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- elem2.bst
+
+sources:
+- kind: local
+ path: files/file3
diff --git a/tests/frontend/workspaced-build-dep/elements/elem4.bst b/tests/frontend/workspaced-build-dep/elements/elem4.bst
new file mode 100644
index 000000000..a04f509a6
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/elem4.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- stack.bst
+
+sources:
+- kind: local
+ path: files/file4
diff --git a/tests/frontend/workspaced-build-dep/elements/elem5.bst b/tests/frontend/workspaced-build-dep/elements/elem5.bst
new file mode 100644
index 000000000..4fb3af822
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/elem5.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- elem3.bst
+
+sources:
+- kind: local
+ path: files/file4
diff --git a/tests/frontend/workspaced-build-dep/elements/stack.bst b/tests/frontend/workspaced-build-dep/elements/stack.bst
new file mode 100644
index 000000000..b4c6002f0
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/elements/stack.bst
@@ -0,0 +1,4 @@
+kind: stack
+
+depends:
+- elem3.bst
diff --git a/tests/frontend/workspaced-build-dep/files/file1 b/tests/frontend/workspaced-build-dep/files/file1
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/files/file1
diff --git a/tests/frontend/workspaced-build-dep/files/file2 b/tests/frontend/workspaced-build-dep/files/file2
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/files/file2
diff --git a/tests/frontend/workspaced-build-dep/files/file3 b/tests/frontend/workspaced-build-dep/files/file3
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/files/file3
diff --git a/tests/frontend/workspaced-build-dep/files/file4 b/tests/frontend/workspaced-build-dep/files/file4
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/files/file4
diff --git a/tests/frontend/workspaced-build-dep/project.conf b/tests/frontend/workspaced-build-dep/project.conf
new file mode 100644
index 000000000..e017957da
--- /dev/null
+++ b/tests/frontend/workspaced-build-dep/project.conf
@@ -0,0 +1,8 @@
+# Unique project name
+name: test
+
+# Required BuildStream format version
+format-version: 12
+
+# Subdirectory where elements are stored
+element-path: elements
diff --git a/tests/frontend/workspaced-runtime-dep/elements/elem1.bst b/tests/frontend/workspaced-runtime-dep/elements/elem1.bst
new file mode 100644
index 000000000..eed39a95a
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/elem1.bst
@@ -0,0 +1,5 @@
+kind: import
+
+sources:
+- kind: local
+ path: files/file1
diff --git a/tests/frontend/workspaced-runtime-dep/elements/elem2.bst b/tests/frontend/workspaced-runtime-dep/elements/elem2.bst
new file mode 100644
index 000000000..a841b9fd0
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/elem2.bst
@@ -0,0 +1,8 @@
+kind: import
+
+runtime-depends:
+- elem1.bst
+
+sources:
+- kind: local
+ path: files/file2
diff --git a/tests/frontend/workspaced-runtime-dep/elements/elem3.bst b/tests/frontend/workspaced-runtime-dep/elements/elem3.bst
new file mode 100644
index 000000000..d817f4bb0
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/elem3.bst
@@ -0,0 +1,8 @@
+kind: import
+
+runtime-depends:
+- elem2.bst
+
+sources:
+- kind: local
+ path: files/file3
diff --git a/tests/frontend/workspaced-runtime-dep/elements/elem4.bst b/tests/frontend/workspaced-runtime-dep/elements/elem4.bst
new file mode 100644
index 000000000..a04f509a6
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/elem4.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- stack.bst
+
+sources:
+- kind: local
+ path: files/file4
diff --git a/tests/frontend/workspaced-runtime-dep/elements/elem5.bst b/tests/frontend/workspaced-runtime-dep/elements/elem5.bst
new file mode 100644
index 000000000..4fb3af822
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/elem5.bst
@@ -0,0 +1,8 @@
+kind: import
+
+build-depends:
+- elem3.bst
+
+sources:
+- kind: local
+ path: files/file4
diff --git a/tests/frontend/workspaced-runtime-dep/elements/stack.bst b/tests/frontend/workspaced-runtime-dep/elements/stack.bst
new file mode 100644
index 000000000..b4c6002f0
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/elements/stack.bst
@@ -0,0 +1,4 @@
+kind: stack
+
+depends:
+- elem3.bst
diff --git a/tests/frontend/workspaced-runtime-dep/files/file1 b/tests/frontend/workspaced-runtime-dep/files/file1
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/files/file1
diff --git a/tests/frontend/workspaced-runtime-dep/files/file2 b/tests/frontend/workspaced-runtime-dep/files/file2
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/files/file2
diff --git a/tests/frontend/workspaced-runtime-dep/files/file3 b/tests/frontend/workspaced-runtime-dep/files/file3
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/files/file3
diff --git a/tests/frontend/workspaced-runtime-dep/files/file4 b/tests/frontend/workspaced-runtime-dep/files/file4
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/files/file4
diff --git a/tests/frontend/workspaced-runtime-dep/project.conf b/tests/frontend/workspaced-runtime-dep/project.conf
new file mode 100644
index 000000000..e017957da
--- /dev/null
+++ b/tests/frontend/workspaced-runtime-dep/project.conf
@@ -0,0 +1,8 @@
+# Unique project name
+name: test
+
+# Required BuildStream format version
+format-version: 12
+
+# Subdirectory where elements are stored
+element-path: elements