summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/completions/completions.py25
-rw-r--r--tests/context/context.py17
-rw-r--r--tests/examples/__init__.py0
-rw-r--r--tests/examples/autotools.py47
-rw-r--r--tests/examples/first-project.py29
-rw-r--r--tests/examples/integration-commands.py36
-rw-r--r--tests/examples/running-commands.py36
-rw-r--r--tests/format/project.py15
-rw-r--r--tests/format/project/project-from-subdir/manual.bst1
-rw-r--r--tests/format/project/project-from-subdir/project.conf4
-rw-r--r--tests/format/project/project-from-subdir/subdirectory/README1
-rw-r--r--tests/frontend/buildcheckout.py19
-rw-r--r--tests/frontend/cross_junction_workspace.py117
-rw-r--r--tests/frontend/fetch.py38
-rw-r--r--tests/frontend/pull.py98
-rw-r--r--tests/frontend/push.py7
-rw-r--r--tests/frontend/show.py47
-rw-r--r--tests/frontend/track.py43
-rw-r--r--tests/frontend/track_cross_junction.py156
-rw-r--r--tests/integration/make.py47
-rw-r--r--tests/integration/project/elements/make/makehello.bst10
-rw-r--r--tests/integration/project/elements/sandbox-bwrap/base-with-tmp.bst6
-rw-r--r--tests/integration/project/elements/sandbox-bwrap/test-cleanup.bst13
-rw-r--r--tests/integration/project/files/base-with-tmp/tmp/dummy1
-rw-r--r--tests/integration/project/files/makehello.tar.gzbin0 -> 432 bytes
-rw-r--r--tests/integration/sandbox-bwrap.py31
-rw-r--r--tests/loader/basics.py12
-rw-r--r--tests/loader/junctions.py40
-rw-r--r--tests/sandboxes/missing-command.py19
-rw-r--r--tests/sandboxes/missing-command/no-runtime.bst1
-rw-r--r--tests/sandboxes/missing-command/project.conf1
-rw-r--r--tests/testutils/__init__.py1
-rw-r--r--tests/testutils/element_generators.py40
33 files changed, 917 insertions, 41 deletions
diff --git a/tests/completions/completions.py b/tests/completions/completions.py
index cc98cb940..7c169c2d2 100644
--- a/tests/completions/completions.py
+++ b/tests/completions/completions.py
@@ -9,6 +9,7 @@ MAIN_COMMANDS = [
'build ',
'checkout ',
'fetch ',
+ 'help ',
'init ',
'pull ',
'push ',
@@ -174,8 +175,9 @@ def test_option_directory(datafiles, cli, cmd, word_idx, expected, subdir):
['compose-all.bst ', 'compose-include-bin.bst ', 'compose-exclude-dev.bst '], None),
# When running from the files subdir
- ('project', 'bst show ', 2, [], 'files'),
- ('project', 'bst build com', 2, [], 'files'),
+ ('project', 'bst show ', 2, [e + ' ' for e in PROJECT_ELEMENTS], 'files'),
+ ('project', 'bst build com', 2,
+ ['compose-all.bst ', 'compose-include-bin.bst ', 'compose-exclude-dev.bst '], 'files'),
# When passing the project directory
('project', 'bst --directory ../ show ', 4, [e + ' ' for e in PROJECT_ELEMENTS], 'files'),
@@ -193,8 +195,10 @@ def test_option_directory(datafiles, cli, cmd, word_idx, expected, subdir):
['compose-all.bst ', 'compose-include-bin.bst ', 'compose-exclude-dev.bst '], None),
# When running from the files subdir
- ('no-element-path', 'bst show ', 2, [], 'files'),
- ('no-element-path', 'bst build com', 2, [], 'files'),
+ ('no-element-path', 'bst show ', 2,
+ [e + ' ' for e in (PROJECT_ELEMENTS + ['project.conf'])] + ['files/'], 'files'),
+ ('no-element-path', 'bst build com', 2,
+ ['compose-all.bst ', 'compose-include-bin.bst ', 'compose-exclude-dev.bst '], 'files'),
# When passing the project directory
('no-element-path', 'bst --directory ../ show ', 4,
@@ -213,3 +217,16 @@ def test_argument_element(datafiles, cli, project, cmd, word_idx, expected, subd
if subdir:
cwd = os.path.join(cwd, subdir)
assert_completion(cli, cmd, word_idx, expected, cwd=cwd)
+
+
+@pytest.mark.parametrize("cmd,word_idx,expected", [
+ ('bst he', 1, ['help ']),
+ ('bst help ', 2, MAIN_COMMANDS),
+ ('bst help fe', 2, ['fetch ']),
+ ('bst help p', 2, ['pull ', 'push ']),
+ ('bst help p', 2, ['pull ', 'push ']),
+ ('bst help w', 2, ['workspace ']),
+ ('bst help workspace ', 3, WORKSPACE_COMMANDS),
+])
+def test_help_commands(cli, cmd, word_idx, expected):
+ assert_completion(cli, cmd, word_idx, expected)
diff --git a/tests/context/context.py b/tests/context/context.py
index 0dc6c588b..35428105b 100644
--- a/tests/context/context.py
+++ b/tests/context/context.py
@@ -47,6 +47,23 @@ def test_context_load(context_fixture):
assert(context.logdir == os.path.join(cache_home, 'buildstream', 'logs'))
+# Assert that a changed XDG_CACHE_HOME doesn't cause issues
+def test_context_load_envvar(context_fixture):
+ os.environ['XDG_CACHE_HOME'] = '/some/path/'
+
+ context = context_fixture['context']
+ assert(isinstance(context, Context))
+
+ context.load(config=os.devnull)
+ assert(context.sourcedir == os.path.join('/', 'some', 'path', 'buildstream', 'sources'))
+ assert(context.builddir == os.path.join('/', 'some', 'path', 'buildstream', 'build'))
+ assert(context.artifactdir == os.path.join('/', 'some', 'path', 'buildstream', 'artifacts'))
+ assert(context.logdir == os.path.join('/', 'some', 'path', 'buildstream', 'logs'))
+
+ # Reset the environment variable
+ del os.environ['XDG_CACHE_HOME']
+
+
# Test that values in a user specified config file
# override the defaults
@pytest.mark.datafiles(os.path.join(DATA_DIR))
diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/examples/__init__.py
diff --git a/tests/examples/autotools.py b/tests/examples/autotools.py
new file mode 100644
index 000000000..c774776fb
--- /dev/null
+++ b/tests/examples/autotools.py
@@ -0,0 +1,47 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+from tests.testutils.site import IS_LINUX
+
+pytestmark = pytest.mark.integration
+
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'autotools'
+)
+
+
+# Tests a build of the autotools amhello project on a alpine-linux base runtime
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_autotools_build(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ checkout = os.path.join(cli.directory, 'checkout')
+
+ # Check that the project can be built correctly.
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ result.assert_success()
+
+ result = cli.run(project=project, args=['checkout', 'hello.bst', checkout])
+ result.assert_success()
+
+ assert_contains(checkout, ['/usr', '/usr/lib', '/usr/bin',
+ '/usr/share', '/usr/lib/debug',
+ '/usr/lib/debug/hello', '/usr/bin/hello',
+ '/usr/share/doc', '/usr/share/doc/amhello',
+ '/usr/share/doc/amhello/README'])
+
+
+# Test running an executable built with autotools.
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_autotools_run(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ result.assert_success()
+
+ result = cli.run(project=project, args=['shell', 'hello.bst', 'hello'])
+ result.assert_success()
+ assert result.output == 'Hello World!\nThis is amhello 1.0.\n'
diff --git a/tests/examples/first-project.py b/tests/examples/first-project.py
new file mode 100644
index 000000000..dac181423
--- /dev/null
+++ b/tests/examples/first-project.py
@@ -0,0 +1,29 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+from tests.testutils.site import IS_LINUX
+
+
+pytestmark = pytest.mark.integration
+
+
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'first-project'
+)
+
+
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_first_project_build_checkout(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ checkout = os.path.join(cli.directory, 'checkout')
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ assert result.exit_code == 0
+
+ result = cli.run(project=project, args=['checkout', 'hello.bst', checkout])
+ assert result.exit_code == 0
+
+ assert_contains(checkout, ['/hello.world'])
diff --git a/tests/examples/integration-commands.py b/tests/examples/integration-commands.py
new file mode 100644
index 000000000..32ef763eb
--- /dev/null
+++ b/tests/examples/integration-commands.py
@@ -0,0 +1,36 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+from tests.testutils.site import IS_LINUX
+
+
+pytestmark = pytest.mark.integration
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'integration-commands'
+)
+
+
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_integration_commands_build(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ checkout = os.path.join(cli.directory, 'checkout')
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ assert result.exit_code == 0
+
+
+# Test running the executable
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_integration_commands_run(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ assert result.exit_code == 0
+
+ result = cli.run(project=project, args=['shell', 'hello.bst', '--', 'hello', 'pony'])
+ assert result.exit_code == 0
+ assert result.output == 'Hello pony\n'
diff --git a/tests/examples/running-commands.py b/tests/examples/running-commands.py
new file mode 100644
index 000000000..95f645d77
--- /dev/null
+++ b/tests/examples/running-commands.py
@@ -0,0 +1,36 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+from tests.testutils.site import IS_LINUX
+
+
+pytestmark = pytest.mark.integration
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'running-commands'
+)
+
+
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_running_commands_build(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ checkout = os.path.join(cli.directory, 'checkout')
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ assert result.exit_code == 0
+
+
+# Test running the executable
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_running_commands_run(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+
+ result = cli.run(project=project, args=['build', 'hello.bst'])
+ assert result.exit_code == 0
+
+ result = cli.run(project=project, args=['shell', 'hello.bst', '--', 'hello'])
+ assert result.exit_code == 0
+ assert result.output == 'Hello World\n'
diff --git a/tests/format/project.py b/tests/format/project.py
index b8e411351..9d595981b 100644
--- a/tests/format/project.py
+++ b/tests/format/project.py
@@ -55,6 +55,21 @@ def test_load_default_project(cli, datafiles):
@pytest.mark.datafiles(os.path.join(DATA_DIR))
+def test_load_project_from_subdir(cli, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename, 'project-from-subdir')
+ result = cli.run(
+ project=project,
+ cwd=os.path.join(project, 'subdirectory'),
+ args=['show', '--format', '%{env}', 'manual.bst'])
+ result.assert_success()
+
+ # Read back some of our project defaults from the env
+ env = _yaml.load_data(result.output)
+ assert (env['USER'] == "tomjon")
+ assert (env['TERM'] == "dumb")
+
+
+@pytest.mark.datafiles(os.path.join(DATA_DIR))
def test_override_project_path(cli, datafiles):
project = os.path.join(datafiles.dirname, datafiles.basename, "overridepath")
result = cli.run(project=project, args=[
diff --git a/tests/format/project/project-from-subdir/manual.bst b/tests/format/project/project-from-subdir/manual.bst
new file mode 100644
index 000000000..4d7f70266
--- /dev/null
+++ b/tests/format/project/project-from-subdir/manual.bst
@@ -0,0 +1 @@
+kind: manual
diff --git a/tests/format/project/project-from-subdir/project.conf b/tests/format/project/project-from-subdir/project.conf
new file mode 100644
index 000000000..fd3134c58
--- /dev/null
+++ b/tests/format/project/project-from-subdir/project.conf
@@ -0,0 +1,4 @@
+# Basic project configuration that doesnt override anything
+#
+
+name: pony
diff --git a/tests/format/project/project-from-subdir/subdirectory/README b/tests/format/project/project-from-subdir/subdirectory/README
new file mode 100644
index 000000000..b32d37708
--- /dev/null
+++ b/tests/format/project/project-from-subdir/subdirectory/README
@@ -0,0 +1 @@
+This directory is used to test running commands from a project subdirectory.
diff --git a/tests/frontend/buildcheckout.py b/tests/frontend/buildcheckout.py
index 3eb98139f..5b46d3d52 100644
--- a/tests/frontend/buildcheckout.py
+++ b/tests/frontend/buildcheckout.py
@@ -390,3 +390,22 @@ def test_build_checkout_workspaced_junction(cli, tmpdir, datafiles):
with open(filename, 'r') as f:
contents = f.read()
assert contents == 'animal=Horsy\n'
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_build_checkout_cross_junction(datafiles, cli, tmpdir):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ subproject_path = os.path.join(project, 'files', 'sub-project')
+ junction_path = os.path.join(project, 'elements', 'junction.bst')
+ checkout = os.path.join(cli.directory, 'checkout')
+
+ generate_junction(tmpdir, subproject_path, junction_path)
+
+ result = cli.run(project=project, args=['build', 'junction.bst:import-etc.bst'])
+ result.assert_success()
+
+ result = cli.run(project=project, args=['checkout', 'junction.bst:import-etc.bst', checkout])
+ result.assert_success()
+
+ filename = os.path.join(checkout, 'etc', 'animal.conf')
+ assert os.path.exists(filename)
diff --git a/tests/frontend/cross_junction_workspace.py b/tests/frontend/cross_junction_workspace.py
new file mode 100644
index 000000000..eb2bc2eb8
--- /dev/null
+++ b/tests/frontend/cross_junction_workspace.py
@@ -0,0 +1,117 @@
+import os
+from tests.testutils import cli, create_repo
+from buildstream import _yaml
+
+
+def prepare_junction_project(cli, tmpdir):
+ main_project = tmpdir.join("main")
+ sub_project = tmpdir.join("sub")
+ os.makedirs(str(main_project))
+ os.makedirs(str(sub_project))
+
+ _yaml.dump({'name': 'main'}, str(main_project.join("project.conf")))
+ _yaml.dump({'name': 'sub'}, str(sub_project.join("project.conf")))
+
+ import_dir = tmpdir.join("import")
+ os.makedirs(str(import_dir))
+ with open(str(import_dir.join("hello.txt")), "w") as f:
+ f.write("hello!")
+
+ import_repo_dir = tmpdir.join("import_repo")
+ os.makedirs(str(import_repo_dir))
+ import_repo = create_repo("git", str(import_repo_dir))
+ import_ref = import_repo.create(str(import_dir))
+
+ _yaml.dump({'kind': 'import',
+ 'sources': [import_repo.source_config(ref=import_ref)]},
+ str(sub_project.join("data.bst")))
+
+ sub_repo_dir = tmpdir.join("sub_repo")
+ os.makedirs(str(sub_repo_dir))
+ sub_repo = create_repo("git", str(sub_repo_dir))
+ sub_ref = sub_repo.create(str(sub_project))
+
+ _yaml.dump({'kind': 'junction',
+ 'sources': [sub_repo.source_config(ref=sub_ref)]},
+ str(main_project.join("sub.bst")))
+
+ args = ['fetch', 'sub.bst']
+ result = cli.run(project=str(main_project), args=args)
+ result.assert_success()
+
+ return str(main_project)
+
+
+def open_cross_junction(cli, tmpdir):
+ project = prepare_junction_project(cli, tmpdir)
+ workspace = tmpdir.join("workspace")
+
+ element = 'sub.bst:data.bst'
+ args = ['workspace', 'open', element, str(workspace)]
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ assert cli.get_element_state(project, element) == 'buildable'
+ assert os.path.exists(str(workspace.join('hello.txt')))
+
+ return project, workspace
+
+
+def test_open_cross_junction(cli, tmpdir):
+ open_cross_junction(cli, tmpdir)
+
+
+def test_list_cross_junction(cli, tmpdir):
+ project, workspace = open_cross_junction(cli, tmpdir)
+
+ element = 'sub.bst:data.bst'
+
+ args = ['workspace', 'list']
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ loaded = _yaml.load_data(result.output)
+ assert isinstance(loaded.get('workspaces'), list)
+ workspaces = loaded['workspaces']
+ assert len(workspaces) == 1
+ assert 'element' in workspaces[0]
+ assert workspaces[0]['element'] == element
+
+
+def test_close_cross_junction(cli, tmpdir):
+ project, workspace = open_cross_junction(cli, tmpdir)
+
+ element = 'sub.bst:data.bst'
+ args = ['workspace', 'close', '--remove-dir', element]
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ assert not os.path.exists(str(workspace))
+
+ args = ['workspace', 'list']
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ loaded = _yaml.load_data(result.output)
+ assert isinstance(loaded.get('workspaces'), list)
+ workspaces = loaded['workspaces']
+ assert len(workspaces) == 0
+
+
+def test_close_all_cross_junction(cli, tmpdir):
+ project, workspace = open_cross_junction(cli, tmpdir)
+
+ args = ['workspace', 'close', '--remove-dir', '--all']
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ assert not os.path.exists(str(workspace))
+
+ args = ['workspace', 'list']
+ result = cli.run(project=project, args=args)
+ result.assert_success()
+
+ loaded = _yaml.load_data(result.output)
+ assert isinstance(loaded.get('workspaces'), list)
+ workspaces = loaded['workspaces']
+ assert len(workspaces) == 0
diff --git a/tests/frontend/fetch.py b/tests/frontend/fetch.py
index e074dadae..ee3a3c3d5 100644
--- a/tests/frontend/fetch.py
+++ b/tests/frontend/fetch.py
@@ -157,3 +157,41 @@ def test_inconsistent_junction(cli, tmpdir, datafiles, ref_storage):
# informing the user to track the junction first
result = cli.run(project=project, args=['fetch', 'junction-dep.bst'])
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.SUBPROJECT_INCONSISTENT)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+@pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')])
+@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
+def test_fetch_cross_junction(cli, tmpdir, datafiles, ref_storage, kind):
+ project = str(datafiles)
+ subproject_path = os.path.join(project, 'files', 'sub-project')
+ junction_path = os.path.join(project, 'elements', 'junction.bst')
+
+ import_etc_path = os.path.join(subproject_path, 'elements', 'import-etc-repo.bst')
+ etc_files_path = os.path.join(subproject_path, 'files', 'etc-files')
+
+ repo = create_repo(kind, str(tmpdir.join('import-etc')))
+ ref = repo.create(etc_files_path)
+
+ element = {
+ 'kind': 'import',
+ 'sources': [
+ repo.source_config(ref=(ref if ref_storage == 'inline' else None))
+ ]
+ }
+ _yaml.dump(element, import_etc_path)
+
+ configure_project(project, {
+ 'ref-storage': ref_storage
+ })
+
+ generate_junction(tmpdir, subproject_path, junction_path, store_ref=(ref_storage == 'inline'))
+
+ if ref_storage == 'project.refs':
+ result = cli.run(project=project, args=['track', 'junction.bst'])
+ result.assert_success()
+ result = cli.run(project=project, args=['track', 'junction.bst:import-etc.bst'])
+ result.assert_success()
+
+ result = cli.run(project=project, args=['fetch', 'junction.bst:import-etc.bst'])
+ result.assert_success()
diff --git a/tests/frontend/pull.py b/tests/frontend/pull.py
index 0d3890993..411ac1b31 100644
--- a/tests/frontend/pull.py
+++ b/tests/frontend/pull.py
@@ -3,6 +3,8 @@ import shutil
import pytest
from tests.testutils import cli, create_artifact_share
+from . import generate_junction
+
# Project directory
DATA_DIR = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
@@ -226,41 +228,73 @@ def test_push_pull_non_strict(cli, tmpdir, datafiles):
@pytest.mark.datafiles(DATA_DIR)
def test_push_pull_track_non_strict(cli, tmpdir, datafiles):
project = os.path.join(datafiles.dirname, datafiles.basename)
+ share = create_artifact_share(os.path.join(str(tmpdir), 'artifactshare'))
+
+ # First build the target element and push to the remote.
+ cli.configure({
+ 'artifacts': {'url': share.repo, 'push': True},
+ 'projects': {
+ 'test': {'strict': False}
+ }
+ })
+ result = cli.run(project=project, args=['build', 'target.bst'])
+ result.assert_success()
+ assert cli.get_element_state(project, 'target.bst') == 'cached'
+
+ # Assert that everything is now cached in the remote.
+ share.update_summary()
+ all_elements = {'target.bst', 'import-bin.bst', 'import-dev.bst', 'compose-all.bst'}
+ for element_name in all_elements:
+ assert_shared(cli, share, project, element_name)
+
+ # Now we've pushed, delete the user's local artifact cache
+ # directory and try to redownload it from the share
+ #
+ artifacts = os.path.join(cli.directory, 'artifacts')
+ shutil.rmtree(artifacts)
- with create_artifact_share(os.path.join(str(tmpdir), 'artifactshare')) as share:
+ # Assert that nothing is cached locally anymore
+ for element_name in all_elements:
+ assert cli.get_element_state(project, element_name) != 'cached'
- # First build the target element and push to the remote.
- cli.configure({
- 'artifacts': {'url': share.repo, 'push': True},
- 'projects': {
- 'test': {'strict': False}
- }
- })
- result = cli.run(project=project, args=['build', 'target.bst'])
- result.assert_success()
- assert cli.get_element_state(project, 'target.bst') == 'cached'
+ # Now try bst build with tracking and pulling.
+ # Tracking will be skipped for target.bst as it doesn't have any sources.
+ # With the non-strict build plan target.bst immediately enters the pull queue.
+ # However, pulling has to be deferred until the dependencies have been
+ # tracked as the strict cache key needs to be calculated before querying
+ # the caches.
+ result = cli.run(project=project, args=['build', '--track-all', '--all', 'target.bst'])
+ result.assert_success()
+ assert set(result.get_pulled_elements()) == all_elements
- # Assert that everything is now cached in the remote.
- all_elements = {'target.bst', 'import-bin.bst', 'import-dev.bst', 'compose-all.bst'}
- for element_name in all_elements:
- assert_shared(cli, share, project, element_name)
- # Now we've pushed, delete the user's local artifact cache
- # directory and try to redownload it from the share
- #
- artifacts = os.path.join(cli.directory, 'artifacts')
- shutil.rmtree(artifacts)
+@pytest.mark.skipif(not IS_LINUX, reason='Only available on linux')
+@pytest.mark.datafiles(DATA_DIR)
+def test_push_pull_cross_junction(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ share = create_artifact_share(os.path.join(str(tmpdir), 'artifactshare'))
+ subproject_path = os.path.join(project, 'files', 'sub-project')
+ junction_path = os.path.join(project, 'elements', 'junction.bst')
- # Assert that nothing is cached locally anymore
- for element_name in all_elements:
- assert cli.get_element_state(project, element_name) != 'cached'
+ generate_junction(tmpdir, subproject_path, junction_path, store_ref=True)
- # Now try bst build with tracking and pulling.
- # Tracking will be skipped for target.bst as it doesn't have any sources.
- # With the non-strict build plan target.bst immediately enters the pull queue.
- # However, pulling has to be deferred until the dependencies have been
- # tracked as the strict cache key needs to be calculated before querying
- # the caches.
- result = cli.run(project=project, args=['build', '--track-all', '--all', 'target.bst'])
- result.assert_success()
- assert set(result.get_pulled_elements()) == all_elements
+ # First build the target element and push to the remote.
+ cli.configure({
+ 'artifacts': {'url': share.repo, 'push': True}
+ })
+ result = cli.run(project=project, args=['build', 'junction.bst:import-etc.bst'])
+ result.assert_success()
+ assert cli.get_element_state(project, 'junction.bst:import-etc.bst') == 'cached'
+
+ cache_dir = os.path.join(project, 'cache', 'artifacts')
+ shutil.rmtree(cache_dir)
+
+ share.update_summary()
+ assert cli.get_element_state(project, 'junction.bst:import-etc.bst') == 'buildable'
+
+ # Now try bst pull
+ result = cli.run(project=project, args=['pull', 'junction.bst:import-etc.bst'])
+ result.assert_success()
+
+ # And assert that it's again in the local cache, without having built
+ assert cli.get_element_state(project, 'junction.bst:import-etc.bst') == 'cached'
diff --git a/tests/frontend/push.py b/tests/frontend/push.py
index 459c340bc..076324ce1 100644
--- a/tests/frontend/push.py
+++ b/tests/frontend/push.py
@@ -1,7 +1,12 @@
import os
import pytest
+from collections import namedtuple
+from unittest.mock import MagicMock
+
from buildstream._exceptions import ErrorDomain
-from tests.testutils import cli, create_artifact_share
+from tests.testutils import cli, create_artifact_share, create_element_size
+from tests.testutils.site import IS_LINUX
+from . import configure_project, generate_junction
# Project directory
DATA_DIR = os.path.join(
diff --git a/tests/frontend/show.py b/tests/frontend/show.py
index 719dadbf4..0276961ab 100644
--- a/tests/frontend/show.py
+++ b/tests/frontend/show.py
@@ -111,7 +111,8 @@ def test_target_is_dependency(cli, tmpdir, datafiles):
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')])
-def test_unfetched_junction(cli, tmpdir, datafiles, ref_storage):
+@pytest.mark.parametrize("element_name", ['junction-dep.bst', 'junction.bst:import-etc.bst'])
+def test_unfetched_junction(cli, tmpdir, datafiles, ref_storage, element_name):
project = os.path.join(datafiles.dirname, datafiles.basename)
subproject_path = os.path.join(project, 'files', 'sub-project')
junction_path = os.path.join(project, 'elements', 'junction.bst')
@@ -155,14 +156,15 @@ def test_unfetched_junction(cli, tmpdir, datafiles, ref_storage):
# Assert the correct error when trying to show the pipeline
result = cli.run(project=project, silent=True, args=[
- 'show', 'junction-dep.bst'])
+ 'show', element_name])
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.SUBPROJECT_FETCH_NEEDED)
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')])
-def test_inconsistent_junction(cli, tmpdir, datafiles, ref_storage):
+@pytest.mark.parametrize("element_name", ['junction-dep.bst', 'junction.bst:import-etc.bst'])
+def test_inconsistent_junction(cli, tmpdir, datafiles, ref_storage, element_name):
project = os.path.join(datafiles.dirname, datafiles.basename)
subproject_path = os.path.join(project, 'files', 'sub-project')
junction_path = os.path.join(project, 'elements', 'junction.bst')
@@ -190,6 +192,43 @@ def test_inconsistent_junction(cli, tmpdir, datafiles, ref_storage):
# Assert the correct error when trying to show the pipeline
result = cli.run(project=project, silent=True, args=[
- 'show', 'junction-dep.bst'])
+ 'show', element_name])
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.SUBPROJECT_INCONSISTENT)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+@pytest.mark.parametrize("element_name", ['junction-dep.bst', 'junction.bst:import-etc.bst'])
+def test_fetched_junction(cli, tmpdir, datafiles, element_name):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ subproject_path = os.path.join(project, 'files', 'sub-project')
+ junction_path = os.path.join(project, 'elements', 'junction.bst')
+ element_path = os.path.join(project, 'elements', 'junction-dep.bst')
+
+ # Create a repo to hold the subproject and generate a junction element for it
+ generate_junction(tmpdir, subproject_path, junction_path, store_ref=True)
+
+ # Create a stack element to depend on a cross junction element
+ #
+ element = {
+ 'kind': 'stack',
+ 'depends': [
+ {
+ 'junction': 'junction.bst',
+ 'filename': 'import-etc.bst'
+ }
+ ]
+ }
+ _yaml.dump(element, element_path)
+
+ result = cli.run(project=project, silent=True, args=[
+ 'fetch', 'junction.bst'])
+
+ result.assert_success()
+
+ # Assert the correct error when trying to show the pipeline
+ result = cli.run(project=project, silent=True, args=[
+ 'show', '--format', '%{name}-%{state}', element_name])
+
+ results = result.output.strip().splitlines()
+ assert 'junction.bst:import-etc.bst-buildable' in results
diff --git a/tests/frontend/track.py b/tests/frontend/track.py
index 2defc2349..51768d650 100644
--- a/tests/frontend/track.py
+++ b/tests/frontend/track.py
@@ -437,3 +437,46 @@ def test_junction_element(cli, tmpdir, datafiles, ref_storage):
# Now assert element state (via bst show under the hood) of the dep again
assert cli.get_element_state(project, 'junction-dep.bst') == 'waiting'
+
+
+@pytest.mark.datafiles(DATA_DIR)
+@pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')])
+@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
+def test_cross_junction(cli, tmpdir, datafiles, ref_storage, kind):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ subproject_path = os.path.join(project, 'files', 'sub-project')
+ junction_path = os.path.join(project, 'elements', 'junction.bst')
+ etc_files = os.path.join(subproject_path, 'files', 'etc-files')
+ repo_element_path = os.path.join(subproject_path, 'elements',
+ 'import-etc-repo.bst')
+
+ configure_project(project, {
+ 'ref-storage': ref_storage
+ })
+
+ repo = create_repo(kind, str(tmpdir.join('element_repo')))
+ ref = repo.create(etc_files)
+
+ generate_element(repo, repo_element_path)
+
+ generate_junction(str(tmpdir.join('junction_repo')),
+ subproject_path, junction_path, store_ref=False)
+
+ # Track the junction itself first.
+ result = cli.run(project=project, args=['track', 'junction.bst'])
+ result.assert_success()
+
+ assert cli.get_element_state(project, 'junction.bst:import-etc-repo.bst') == 'no reference'
+
+ # Track the cross junction element. -J is not given, it is implied.
+ result = cli.run(project=project, args=['track', 'junction.bst:import-etc-repo.bst'])
+
+ if ref_storage == 'inline':
+ # This is not allowed to track cross junction without project.refs.
+ result.assert_main_error(ErrorDomain.PIPELINE, 'untrackable-sources')
+ else:
+ result.assert_success()
+
+ assert cli.get_element_state(project, 'junction.bst:import-etc-repo.bst') == 'buildable'
+
+ assert os.path.exists(os.path.join(project, 'project.refs'))
diff --git a/tests/frontend/track_cross_junction.py b/tests/frontend/track_cross_junction.py
new file mode 100644
index 000000000..34c39ddd2
--- /dev/null
+++ b/tests/frontend/track_cross_junction.py
@@ -0,0 +1,156 @@
+import os
+import pytest
+from tests.testutils import cli, create_repo, ALL_REPO_KINDS
+from buildstream import _yaml
+
+from . import generate_junction
+
+
+def generate_element(repo, element_path, dep_name=None):
+ element = {
+ 'kind': 'import',
+ 'sources': [
+ repo.source_config()
+ ]
+ }
+ if dep_name:
+ element['depends'] = [dep_name]
+
+ _yaml.dump(element, element_path)
+
+
+def generate_import_element(tmpdir, kind, project, name):
+ element_name = 'import-{}.bst'.format(name)
+ repo_element_path = os.path.join(project, 'elements', element_name)
+ files = str(tmpdir.join("imported_files_{}".format(name)))
+ os.makedirs(files)
+
+ with open(os.path.join(files, '{}.txt'.format(name)), 'w') as f:
+ f.write(name)
+
+ subproject_path = os.path.join(str(tmpdir.join('sub-project-{}'.format(name))))
+
+ repo = create_repo(kind, str(tmpdir.join('element_{}_repo'.format(name))))
+ ref = repo.create(files)
+
+ generate_element(repo, repo_element_path)
+
+ return element_name
+
+
+def generate_project(tmpdir, name, config={}):
+ project_name = 'project-{}'.format(name)
+ subproject_path = os.path.join(str(tmpdir.join(project_name)))
+ os.makedirs(os.path.join(subproject_path, 'elements'))
+
+ project_conf = {
+ 'name': name,
+ 'element-path': 'elements'
+ }
+ project_conf.update(config)
+ _yaml.dump(project_conf, os.path.join(subproject_path, 'project.conf'))
+
+ return project_name, subproject_path
+
+
+def generate_simple_stack(project, name, dependencies):
+ element_name = '{}.bst'.format(name)
+ element_path = os.path.join(project, 'elements', element_name)
+ element = {
+ 'kind': 'stack',
+ 'depends': dependencies
+ }
+ _yaml.dump(element, element_path)
+
+ return element_name
+
+
+def generate_cross_element(project, subproject_name, import_name):
+ basename, _ = os.path.splitext(import_name)
+ return generate_simple_stack(project, 'import-{}-{}'.format(subproject_name, basename),
+ [{
+ 'junction': '{}.bst'.format(subproject_name),
+ 'filename': import_name
+ }])
+
+
+@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
+def test_cross_junction_multiple_projects(cli, tmpdir, datafiles, kind):
+ tmpdir = tmpdir.join(kind)
+
+ # Generate 3 projects: main, a, b
+ _, project = generate_project(tmpdir, 'main', {'ref-storage': 'project.refs'})
+ project_a, project_a_path = generate_project(tmpdir, 'a')
+ project_b, project_b_path = generate_project(tmpdir, 'b')
+
+ # Generate an element with a trackable source for each project
+ element_a = generate_import_element(tmpdir, kind, project_a_path, 'a')
+ element_b = generate_import_element(tmpdir, kind, project_b_path, 'b')
+ element_c = generate_import_element(tmpdir, kind, project, 'c')
+
+ # Create some indirections to the elements with dependencies to test --deps
+ stack_a = generate_simple_stack(project_a_path, 'stack-a', [element_a])
+ stack_b = generate_simple_stack(project_b_path, 'stack-b', [element_b])
+
+ # Create junctions for projects a and b in main.
+ junction_a = '{}.bst'.format(project_a)
+ junction_a_path = os.path.join(project, 'elements', junction_a)
+ generate_junction(tmpdir.join('repo_a'), project_a_path, junction_a_path, store_ref=False)
+
+ junction_b = '{}.bst'.format(project_b)
+ junction_b_path = os.path.join(project, 'elements', junction_b)
+ generate_junction(tmpdir.join('repo_b'), project_b_path, junction_b_path, store_ref=False)
+
+ # Track the junctions.
+ result = cli.run(project=project, args=['track', junction_a, junction_b])
+ result.assert_success()
+
+ # Import elements from a and b in to main.
+ imported_a = generate_cross_element(project, project_a, stack_a)
+ imported_b = generate_cross_element(project, project_b, stack_b)
+
+ # Generate a top level stack depending on everything
+ all_bst = generate_simple_stack(project, 'all', [imported_a, imported_b, element_c])
+
+ # Track without following junctions. But explicitly also track the elements in project a.
+ result = cli.run(project=project, args=['track', '--deps', 'all', all_bst, '{}:{}'.format(junction_a, stack_a)])
+ result.assert_success()
+
+ # Elements in project b should not be tracked. But elements in project a and main should.
+ expected = [element_c,
+ '{}:{}'.format(junction_a, element_a)]
+ assert set(result.get_tracked_elements()) == set(expected)
+
+
+@pytest.mark.parametrize("kind", [(kind) for kind in ALL_REPO_KINDS])
+def test_track_exceptions(cli, tmpdir, datafiles, kind):
+ tmpdir = tmpdir.join(kind)
+
+ _, project = generate_project(tmpdir, 'main', {'ref-storage': 'project.refs'})
+ project_a, project_a_path = generate_project(tmpdir, 'a')
+
+ element_a = generate_import_element(tmpdir, kind, project_a_path, 'a')
+ element_b = generate_import_element(tmpdir, kind, project_a_path, 'b')
+
+ all_bst = generate_simple_stack(project_a_path, 'all', [element_a,
+ element_b])
+
+ junction_a = '{}.bst'.format(project_a)
+ junction_a_path = os.path.join(project, 'elements', junction_a)
+ generate_junction(tmpdir.join('repo_a'), project_a_path, junction_a_path, store_ref=False)
+
+ result = cli.run(project=project, args=['track', junction_a])
+ result.assert_success()
+
+ imported_b = generate_cross_element(project, project_a, element_b)
+ indirection = generate_simple_stack(project, 'indirection', [imported_b])
+
+ result = cli.run(project=project,
+ args=['track', '--deps', 'all',
+ '--except', indirection,
+ '{}:{}'.format(junction_a, all_bst), imported_b])
+ result.assert_success()
+
+ expected = ['{}:{}'.format(junction_a, element_a),
+ '{}:{}'.format(junction_a, element_b)]
+ assert set(result.get_tracked_elements()) == set(expected)
diff --git a/tests/integration/make.py b/tests/integration/make.py
new file mode 100644
index 000000000..6928cfdc2
--- /dev/null
+++ b/tests/integration/make.py
@@ -0,0 +1,47 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+
+
+pytestmark = pytest.mark.integration
+
+
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "project"
+)
+
+
+# Test that a make build 'works' - we use the make sample
+# makehello project for this.
+@pytest.mark.integration
+@pytest.mark.datafiles(DATA_DIR)
+def test_make_build(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ checkout = os.path.join(cli.directory, 'checkout')
+ element_name = 'make/makehello.bst'
+
+ result = cli.run(project=project, args=['build', element_name])
+ assert result.exit_code == 0
+
+ result = cli.run(project=project, args=['checkout', element_name, checkout])
+ assert result.exit_code == 0
+
+ assert_contains(checkout, ['/usr', '/usr/bin',
+ '/usr/bin/hello'])
+
+
+# Test running an executable built with make
+@pytest.mark.datafiles(DATA_DIR)
+def test_make_run(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ element_name = 'make/makehello.bst'
+
+ result = cli.run(project=project, args=['build', element_name])
+ assert result.exit_code == 0
+
+ result = cli.run(project=project, args=['shell', element_name, '/usr/bin/hello'])
+ assert result.exit_code == 0
+ assert result.output == 'Hello, world\n'
diff --git a/tests/integration/project/elements/make/makehello.bst b/tests/integration/project/elements/make/makehello.bst
new file mode 100644
index 000000000..4b5c5ac3b
--- /dev/null
+++ b/tests/integration/project/elements/make/makehello.bst
@@ -0,0 +1,10 @@
+kind: make
+description: make test
+
+depends:
+- base.bst
+
+sources:
+- kind: tar
+ url: project_dir:/files/makehello.tar.gz
+ ref: fd342a36503a0a0dd37b81ddb4d2b78bd398d912d813339e0de44a6b6c393b8e
diff --git a/tests/integration/project/elements/sandbox-bwrap/base-with-tmp.bst b/tests/integration/project/elements/sandbox-bwrap/base-with-tmp.bst
new file mode 100644
index 000000000..5c9fa6083
--- /dev/null
+++ b/tests/integration/project/elements/sandbox-bwrap/base-with-tmp.bst
@@ -0,0 +1,6 @@
+kind: import
+description: Base for after-sandbox cleanup test
+
+sources:
+ - kind: local
+ path: files/base-with-tmp/
diff --git a/tests/integration/project/elements/sandbox-bwrap/test-cleanup.bst b/tests/integration/project/elements/sandbox-bwrap/test-cleanup.bst
new file mode 100644
index 000000000..2a89dd3ee
--- /dev/null
+++ b/tests/integration/project/elements/sandbox-bwrap/test-cleanup.bst
@@ -0,0 +1,13 @@
+kind: manual
+description: A dummy project to utilize a base with existing /tmp folder.
+
+depends:
+ - filename: base.bst
+ type: build
+
+ - filename: sandbox-bwrap/base-with-tmp.bst
+
+config:
+ build-commands:
+ - |
+ true
diff --git a/tests/integration/project/files/base-with-tmp/tmp/dummy b/tests/integration/project/files/base-with-tmp/tmp/dummy
new file mode 100644
index 000000000..d18f28449
--- /dev/null
+++ b/tests/integration/project/files/base-with-tmp/tmp/dummy
@@ -0,0 +1 @@
+dummy! \ No newline at end of file
diff --git a/tests/integration/project/files/makehello.tar.gz b/tests/integration/project/files/makehello.tar.gz
new file mode 100644
index 000000000..d0edcb29c
--- /dev/null
+++ b/tests/integration/project/files/makehello.tar.gz
Binary files differ
diff --git a/tests/integration/sandbox-bwrap.py b/tests/integration/sandbox-bwrap.py
new file mode 100644
index 000000000..7d2a18498
--- /dev/null
+++ b/tests/integration/sandbox-bwrap.py
@@ -0,0 +1,31 @@
+import os
+import pytest
+
+from tests.testutils import cli_integration as cli
+from tests.testutils.integration import assert_contains
+from tests.testutils.site import HAVE_BWRAP
+
+
+pytestmark = pytest.mark.integration
+
+
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "project"
+)
+
+
+# Bubblewrap sandbox doesn't remove the dirs it created during its execution,
+# so BuildStream tries to remove them to do good. BuildStream should be extra
+# careful when those folders already exist and should not touch them, though.
+@pytest.mark.integration
+@pytest.mark.skipif(not HAVE_BWRAP, reason='Only available with bubblewrap')
+@pytest.mark.datafiles(DATA_DIR)
+def test_sandbox_bwrap_cleanup_build(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ # This element depends on a base image with non-empty `/tmp` folder.
+ element_name = 'sandbox-bwrap/test-cleanup.bst'
+
+ # Here, BuildStream should not attempt any rmdir etc.
+ result = cli.run(project=project, args=['build', element_name])
+ assert result.exit_code == 0
diff --git a/tests/loader/basics.py b/tests/loader/basics.py
index 008750f70..3526697c5 100644
--- a/tests/loader/basics.py
+++ b/tests/loader/basics.py
@@ -84,3 +84,15 @@ def test_invalid_key(datafiles):
element = loader.load()[0]
assert (exc.value.reason == LoadErrorReason.INVALID_DATA)
+
+
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'onefile'))
+def test_invalid_directory_load(datafiles):
+
+ basedir = os.path.join(datafiles.dirname, datafiles.basename)
+ loader = make_loader(basedir, ['elements/'])
+
+ with pytest.raises(LoadError) as exc:
+ element = loader.load()[0]
+
+ assert (exc.value.reason == LoadErrorReason.LOADING_DIRECTORY)
diff --git a/tests/loader/junctions.py b/tests/loader/junctions.py
index 635a987bd..a02961fb5 100644
--- a/tests/loader/junctions.py
+++ b/tests/loader/junctions.py
@@ -260,3 +260,43 @@ def test_git_build(cli, tmpdir, datafiles):
# Check that the checkout contains the expected files from both projects
assert(os.path.exists(os.path.join(checkoutdir, 'base.txt')))
assert(os.path.exists(os.path.join(checkoutdir, 'foo.txt')))
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_cross_junction_names(cli, tmpdir, datafiles):
+ project = os.path.join(str(datafiles), 'foo')
+ copy_subprojects(project, datafiles, ['base'])
+
+ element_list = cli.get_pipeline(project, ['base.bst:target.bst'])
+ assert 'base.bst:target.bst' in element_list
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_build_git_cross_junction_names(cli, tmpdir, datafiles):
+ project = os.path.join(str(datafiles), 'foo')
+ checkoutdir = os.path.join(str(tmpdir), "checkout")
+
+ # Create the repo from 'base' subdir
+ repo = create_repo('git', str(tmpdir))
+ ref = repo.create(os.path.join(str(datafiles), 'base'))
+
+ # Write out junction element with git source
+ element = {
+ 'kind': 'junction',
+ 'sources': [
+ repo.source_config(ref=ref)
+ ]
+ }
+ _yaml.dump(element, os.path.join(project, 'base.bst'))
+
+ print(element)
+ print(cli.get_pipeline(project, ['base.bst']))
+
+ # Build (with implicit fetch of subproject), checkout
+ result = cli.run(project=project, args=['build', 'base.bst:target.bst'])
+ assert result.exit_code == 0
+ result = cli.run(project=project, args=['checkout', 'base.bst:target.bst', checkoutdir])
+ assert result.exit_code == 0
+
+ # Check that the checkout contains the expected files from both projects
+ assert(os.path.exists(os.path.join(checkoutdir, 'base.txt')))
diff --git a/tests/sandboxes/missing-command.py b/tests/sandboxes/missing-command.py
new file mode 100644
index 000000000..8f210bcec
--- /dev/null
+++ b/tests/sandboxes/missing-command.py
@@ -0,0 +1,19 @@
+import os
+import pytest
+
+from buildstream._exceptions import ErrorDomain
+
+from tests.testutils import cli
+
+
+DATA_DIR = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "missing-command"
+)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_missing_command(cli, tmpdir, datafiles):
+ project = os.path.join(datafiles.dirname, datafiles.basename)
+ result = cli.run(project=project, args=['build', 'no-runtime.bst'])
+ result.assert_task_error(ErrorDomain.SANDBOX, 'missing-command')
diff --git a/tests/sandboxes/missing-command/no-runtime.bst b/tests/sandboxes/missing-command/no-runtime.bst
new file mode 100644
index 000000000..4d7f70266
--- /dev/null
+++ b/tests/sandboxes/missing-command/no-runtime.bst
@@ -0,0 +1 @@
+kind: manual
diff --git a/tests/sandboxes/missing-command/project.conf b/tests/sandboxes/missing-command/project.conf
new file mode 100644
index 000000000..b32753625
--- /dev/null
+++ b/tests/sandboxes/missing-command/project.conf
@@ -0,0 +1 @@
+name: test
diff --git a/tests/testutils/__init__.py b/tests/testutils/__init__.py
index 7e5b792a2..93143b505 100644
--- a/tests/testutils/__init__.py
+++ b/tests/testutils/__init__.py
@@ -1,3 +1,4 @@
from .runcli import cli, cli_integration
from .repo import create_repo, ALL_REPO_KINDS
from .artifactshare import create_artifact_share
+from .element_generators import create_element_size
diff --git a/tests/testutils/element_generators.py b/tests/testutils/element_generators.py
new file mode 100644
index 000000000..3f6090da8
--- /dev/null
+++ b/tests/testutils/element_generators.py
@@ -0,0 +1,40 @@
+import os
+
+from buildstream import _yaml
+
+
+# create_element_size()
+#
+# This will open a "<name>_data" file for writing and write
+# <size> MB of urandom (/dev/urandom) "stuff" into the file.
+# A bst import element file is then created: <name>.bst
+#
+# Args:
+# name: (str) of the element name (e.g. target.bst)
+# path: (str) pathway to the project/elements directory
+# dependencies: A list of strings (can also be an empty list)
+# size: (int) size of the element in bytes
+#
+# Returns:
+# Nothing (creates a .bst file of specified size)
+#
+def create_element_size(name, path, dependencies, size):
+ os.makedirs(path, exist_ok=True)
+
+ # Create a file to be included in this element's artifact
+ with open(os.path.join(path, name + '_data'), 'wb+') as f:
+ f.write(os.urandom(size))
+
+ # Simplest case: We want this file (of specified size) to just
+ # be an import element.
+ element = {
+ 'kind': 'import',
+ 'sources': [
+ {
+ 'kind': 'local',
+ 'path': os.path.join(path, name + '_data')
+ }
+ ],
+ 'depends': dependencies
+ }
+ _yaml.dump(element, os.path.join(path, name))