diff options
author | Tiago Gomes <tiago.avv@gmail.com> | 2018-08-02 12:10:29 +0000 |
---|---|---|
committer | Tiago Gomes <tiago.avv@gmail.com> | 2018-08-02 12:10:29 +0000 |
commit | 8656a65d21bd002b1fd490481e736f1748e8db86 (patch) | |
tree | a54839da778fc48c22fac07c4d64fa8dc7e3ed93 | |
parent | 039d43e43431957a42193f12f06acb7b95c2eae4 (diff) | |
parent | 9f0c8fcfe348533987ec128b85356655edae77f4 (diff) | |
download | buildstream-8656a65d21bd002b1fd490481e736f1748e8db86.tar.gz |
Merge branch 'tiagogomes/issue-195' into 'master'Qinusty/490-artifact-cache-interactivity
Add validation for project paths
See merge request BuildStream/buildstream!593
30 files changed, 492 insertions, 139 deletions
@@ -184,7 +184,7 @@ ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,contextlib.closing,gi.repository.GLib.GError +ignored-classes=optparse.Values,thread._local,_thread._local,contextlib.closing,gi.repository.GLib.GError,pathlib.PurePath # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/buildstream/_exceptions.py b/buildstream/_exceptions.py index e55d942fd..5187357c5 100644 --- a/buildstream/_exceptions.py +++ b/buildstream/_exceptions.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +16,7 @@ # # Authors: # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> from enum import Enum @@ -206,6 +207,13 @@ class LoadErrorReason(Enum): # Try to load a directory not a yaml file LOADING_DIRECTORY = 18 + # A project path leads outside of the project directory + PROJ_PATH_INVALID = 19 + + # A project path points to a file of the not right kind (e.g. a + # socket) + PROJ_PATH_INVALID_KIND = 20 + # LoadError # diff --git a/buildstream/_project.py b/buildstream/_project.py index 1c30fb9bb..3ac562836 100644 --- a/buildstream/_project.py +++ b/buildstream/_project.py @@ -16,6 +16,7 @@ # # Authors: # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> import os import multiprocessing # for cpu_count() @@ -291,7 +292,8 @@ class Project(): self.element_path = os.path.join( self.directory, - _yaml.node_get(config, str, 'element-path') + _yaml.node_get_project_path(config, 'element-path', self.directory, + check_is_dir=True) ) # Load project options @@ -500,8 +502,11 @@ class Project(): if group in origin_dict: del origin_dict[group] if origin_dict['origin'] == 'local': + path = _yaml.node_get_project_path(origin, 'path', + self.directory, + check_is_dir=True) # paths are passed in relative to the project, but must be absolute - origin_dict['path'] = os.path.join(self.directory, origin_dict['path']) + origin_dict['path'] = os.path.join(self.directory, path) destination.append(origin_dict) # _ensure_project_dir() diff --git a/buildstream/_yaml.py b/buildstream/_yaml.py index 0e090e2e7..33ee444aa 100644 --- a/buildstream/_yaml.py +++ b/buildstream/_yaml.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -22,6 +22,7 @@ import collections import string from copy import deepcopy from contextlib import ExitStack +from pathlib import Path from ruamel import yaml from ruamel.yaml.representer import SafeRepresenter, RoundTripRepresenter @@ -392,6 +393,95 @@ def node_get(node, expected_type, key, indices=None, default_value=_get_sentinel return value +# node_get_project_path() +# +# Fetches a project path from a dictionary node and validates it +# +# Paths are asserted to never lead to a directory outside of the project +# directory. In addition, paths can not point to symbolic links, fifos, +# sockets and block/character devices. +# +# The `check_is_file` and `check_is_dir` parameters can be used to +# perform additional validations on the path. Note that an exception +# will always be raised if both parameters are set to ``True``. +# +# Args: +# node (dict): A dictionary loaded from YAML +# key (str): The key whose value contains a path to validate +# project_dir (str): The project directory +# check_is_file (bool): If ``True`` an error will also be raised +# if path does not point to a regular file. +# Defaults to ``False`` +# check_is_dir (bool): If ``True`` an error will be also raised +# if path does not point to a directory. +# Defaults to ``False`` +# Returns: +# (str): The project path +# +# Raises: +# (LoadError): In case that the project path is not valid or does not +# exist +# +def node_get_project_path(node, key, project_dir, *, + check_is_file=False, check_is_dir=False): + path_str = node_get(node, str, key) + path = Path(path_str) + project_dir_path = Path(project_dir) + + provenance = node_get_provenance(node, key=key) + + if (project_dir_path / path).is_symlink(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' must not point to " + "symbolic links " + .format(provenance, path_str)) + + if path.parts and path.parts[0] == '..': + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' first component must " + "not be '..'" + .format(provenance, path_str)) + + try: + if sys.version_info[0] == 3 and sys.version_info[1] < 6: + full_resolved_path = (project_dir_path / path).resolve() + else: + full_resolved_path = (project_dir_path / path).resolve(strict=True) + except FileNotFoundError: + raise LoadError(LoadErrorReason.MISSING_FILE, + "{}: Specified path '{}' does not exist" + .format(provenance, path_str)) + + is_inside = project_dir_path in full_resolved_path.parents or ( + full_resolved_path == project_dir_path) + + if path.is_absolute() or not is_inside: + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID, + "{}: Specified path '{}' must not lead outside of the " + "project directory" + .format(provenance, path_str)) + + if full_resolved_path.is_socket() or ( + full_resolved_path.is_fifo() or + full_resolved_path.is_block_device()): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' points to an unsupported " + "file kind" + .format(provenance, path_str)) + + if check_is_file and not full_resolved_path.is_file(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a regular file" + .format(provenance, path_str)) + + if check_is_dir and not full_resolved_path.is_dir(): + raise LoadError(LoadErrorReason.PROJ_PATH_INVALID_KIND, + "{}: Specified path '{}' is not a directory" + .format(provenance, path_str)) + + return path_str + + # node_items() # # A convenience generator for iterating over loaded key/value diff --git a/buildstream/plugin.py b/buildstream/plugin.py index 155a9500e..836b60834 100644 --- a/buildstream/plugin.py +++ b/buildstream/plugin.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2017 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -335,6 +335,51 @@ class Plugin(): """ return _yaml.node_get(node, expected_type, member_name, default_value=default) + def node_get_project_path(self, node, key, *, + check_is_file=False, check_is_dir=False): + """Fetches a project path from a dictionary node and validates it + + Paths are asserted to never lead to a directory outside of the + project directory. In addition, paths can not point to symbolic + links, fifos, sockets and block/character devices. + + The `check_is_file` and `check_is_dir` parameters can be used to + perform additional validations on the path. Note that an + exception will always be raised if both parameters are set to + ``True``. + + Args: + node (dict): A dictionary loaded from YAML + key (str): The key whose value contains a path to validate + check_is_file (bool): If ``True`` an error will also be raised + if path does not point to a regular file. + Defaults to ``False`` + check_is_dir (bool): If ``True`` an error will also be raised + if path does not point to a directory. + Defaults to ``False`` + + Returns: + (str): The project path + + Raises: + :class:`.LoadError`: In the case that the project path is not + valid or does not exist + + *Since: 1.2* + + **Example:** + + .. code:: python + + path = self.node_get_project_path(node, 'path') + + """ + + return _yaml.node_get_project_path(node, key, + self.__project.directory, + check_is_file=check_is_file, + check_is_dir=check_is_dir) + def node_validate(self, node, valid_keys): """This should be used in :func:`~buildstream.plugin.Plugin.configure` implementations to assert that users have only entered diff --git a/buildstream/plugins/sources/local.py b/buildstream/plugins/sources/local.py index e3b019f1a..058553424 100644 --- a/buildstream/plugins/sources/local.py +++ b/buildstream/plugins/sources/local.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +16,7 @@ # # Authors: # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> """ local - stage local files and directories @@ -36,7 +37,7 @@ local - stage local files and directories """ import os -from buildstream import Source, SourceError, Consistency +from buildstream import Source, Consistency from buildstream import utils @@ -51,14 +52,11 @@ class LocalSource(Source): def configure(self, node): self.node_validate(node, ['path'] + Source.COMMON_CONFIG_KEYS) - - self.path = self.node_get_member(node, str, 'path') + self.path = self.node_get_project_path(node, 'path') self.fullpath = os.path.join(self.get_project_directory(), self.path) def preflight(self): - # Check if the configured file or directory really exists - if not os.path.exists(self.fullpath): - raise SourceError("Specified path '{}' does not exist".format(self.path)) + pass def get_unique_key(self): if self.__unique_key is None: diff --git a/buildstream/plugins/sources/ostree.py b/buildstream/plugins/sources/ostree.py index 94fe5093f..3a841c488 100644 --- a/buildstream/plugins/sources/ostree.py +++ b/buildstream/plugins/sources/ostree.py @@ -1,5 +1,5 @@ # -# Copyright (C) 2016 Codethink Limited +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +16,7 @@ # # Authors: # Andrew Leeming <andrew.leeming@codethink.co.uk> +# Tiago Gomes <tiago.gomes@codethink.co.uk> """ ostree - stage files from an OSTree repository @@ -73,9 +74,10 @@ class OSTreeSource(Source): utils.url_directory_name(self.url)) # (optional) Not all repos are signed. But if they are, get the gpg key - self.gpg_key = self.node_get_member(node, str, 'gpg-key', None) self.gpg_key_path = None - if self.gpg_key is not None: + if self.node_get_member(node, str, 'gpg-key', None): + self.gpg_key = self.node_get_project_path(node, 'gpg-key', + check_is_file=True) self.gpg_key_path = os.path.join(self.get_project_directory(), self.gpg_key) # Our OSTree repo handle diff --git a/buildstream/plugins/sources/patch.py b/buildstream/plugins/sources/patch.py index 11b66b3ea..2fa002080 100644 --- a/buildstream/plugins/sources/patch.py +++ b/buildstream/plugins/sources/patch.py @@ -1,5 +1,6 @@ # # Copyright Bloomberg Finance LP +# Copyright (C) 2018 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,6 +17,7 @@ # # Authors: # Chandan Singh <csingh43@bloomberg.net> +# Tiago Gomes <tiago.gomes@codethink.co.uk> """ patch - apply locally stored patches @@ -52,19 +54,12 @@ class PatchSource(Source): # pylint: disable=attribute-defined-outside-init def configure(self, node): - self.path = self.node_get_member(node, str, "path") + self.path = self.node_get_project_path(node, 'path', + check_is_file=True) self.strip_level = self.node_get_member(node, int, "strip-level", 1) self.fullpath = os.path.join(self.get_project_directory(), self.path) def preflight(self): - # Check if the configured file really exists - if not os.path.exists(self.fullpath): - raise SourceError("Specified path '{}' does not exist".format(self.path), - reason="patch-no-exist") - elif not os.path.isfile(self.fullpath): - raise SourceError("Specified path '{}' must be a file".format(self.path), - reason="patch-not-a-file") - # Check if patch is installed, get the binary at the same time self.host_patch = utils.get_host_tool("patch") diff --git a/doc/examples/junctions/autotools/elements/base.bst b/doc/examples/junctions/autotools/elements/base.bst new file mode 100644 index 000000000..1b85a9e8c --- /dev/null +++ b/doc/examples/junctions/autotools/elements/base.bst @@ -0,0 +1,5 @@ +kind: stack +description: Base stack + +depends: +- base/alpine.bst diff --git a/doc/examples/junctions/autotools/elements/base/alpine.bst b/doc/examples/junctions/autotools/elements/base/alpine.bst new file mode 100644 index 000000000..cf85df5bf --- /dev/null +++ b/doc/examples/junctions/autotools/elements/base/alpine.bst @@ -0,0 +1,13 @@ +kind: import +description: | + + Alpine Linux base runtime + +sources: +- kind: tar + + # This is a post doctored, trimmed down system image + # of the Alpine linux distribution. + # + url: alpine:integration-tests-base.v1.x86_64.tar.xz + ref: 3eb559250ba82b64a68d86d0636a6b127aa5f6d25d3601a79f79214dc9703639 diff --git a/doc/examples/junctions/autotools/elements/hello.bst b/doc/examples/junctions/autotools/elements/hello.bst new file mode 100644 index 000000000..510f5b975 --- /dev/null +++ b/doc/examples/junctions/autotools/elements/hello.bst @@ -0,0 +1,21 @@ +kind: autotools +description: | + + Hello world example from automake + +variables: + + # The hello world example lives in the doc/amhello folder. + # + # Set the %{command-subdir} variable to that location + # and just have the autotools element run it's commands there. + # + command-subdir: doc/amhello + +sources: +- kind: tar + url: gnu:automake-1.16.tar.gz + ref: 80da43bb5665596ee389e6d8b64b4f122ea4b92a685b1dbd813cd1f0e0c2d83f + +depends: +- base.bst diff --git a/doc/examples/junctions/autotools/project.conf b/doc/examples/junctions/autotools/project.conf new file mode 100644 index 000000000..7ee58b589 --- /dev/null +++ b/doc/examples/junctions/autotools/project.conf @@ -0,0 +1,13 @@ +# Unique project name +name: autotools + +# Required BuildStream format version +format-version: 9 + +# Subdirectory where elements are stored +element-path: elements + +# Define some aliases for the tarballs we download +aliases: + alpine: https://gnome7.codethink.co.uk/tarballs/ + gnu: https://ftp.gnu.org/gnu/automake/ diff --git a/doc/examples/junctions/elements/hello-junction.bst b/doc/examples/junctions/elements/hello-junction.bst index dda865ecf..6d01e36a1 100644 --- a/doc/examples/junctions/elements/hello-junction.bst +++ b/doc/examples/junctions/elements/hello-junction.bst @@ -1,8 +1,4 @@ kind: junction - -# Specify the source of the BuildStream project -# We are going to use the autotools examples distributed with BuildStream in the -# doc/examples/autotools directory sources: - kind: local - path: ../autotools + path: autotools diff --git a/doc/source/advanced-features/junction-elements.rst b/doc/source/advanced-features/junction-elements.rst index 929ac1217..81fc01a05 100644 --- a/doc/source/advanced-features/junction-elements.rst +++ b/doc/source/advanced-features/junction-elements.rst @@ -21,8 +21,8 @@ Below is a simple example of bst file for a junction element: .. literalinclude:: ../../examples/junctions/elements/hello-junction.bst :language: yaml -This element imports the autotools example project found in the BuildStream -doc/examples/autotools subdirectory. +This element imports the autotools example subproject found in the +BuildStream doc/examples/junctions/autotools subdirectory. .. note:: diff --git a/tests/artifactcache/expiry.py b/tests/artifactcache/expiry.py index 4c741054b..9c74eb1c4 100644 --- a/tests/artifactcache/expiry.py +++ b/tests/artifactcache/expiry.py @@ -5,7 +5,7 @@ import pytest from buildstream import _yaml from buildstream._exceptions import ErrorDomain, LoadErrorReason -from tests.testutils import cli +from tests.testutils import cli, create_element_size DATA_DIR = os.path.join( @@ -14,32 +14,12 @@ DATA_DIR = os.path.join( ) -def create_element(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)) - - element = { - 'kind': 'import', - 'sources': [ - { - 'kind': 'local', - 'path': os.path.join(path, name + '_data') - } - ], - 'depends': dependencies - } - _yaml.dump(element, os.path.join(path, name)) - - # Ensure that the cache successfully removes an old artifact if we do # not have enough space left. @pytest.mark.datafiles(DATA_DIR) def test_artifact_expires(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' cache_location = os.path.join(project, 'cache', 'artifacts', 'ostree') checkout = os.path.join(project, 'checkout') @@ -52,7 +32,7 @@ def test_artifact_expires(cli, datafiles, tmpdir): # Create an element that uses almost the entire cache (an empty # ostree cache starts at about ~10KiB, so we need a bit of a # buffer) - create_element('target.bst', element_path, [], 6000000) + create_element_size('target.bst', project, element_path, [], 6000000) res = cli.run(project=project, args=['build', 'target.bst']) res.assert_success() @@ -61,7 +41,7 @@ def test_artifact_expires(cli, datafiles, tmpdir): # Our cache should now be almost full. Let's create another # artifact and see if we can cause buildstream to delete the old # one. - create_element('target2.bst', element_path, [], 6000000) + create_element_size('target2.bst', project, element_path, [], 6000000) res = cli.run(project=project, args=['build', 'target2.bst']) res.assert_success() @@ -82,7 +62,7 @@ def test_artifact_expires(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_artifact_too_large(cli, datafiles, tmpdir, size): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' cli.configure({ 'cache': { @@ -91,7 +71,7 @@ def test_artifact_too_large(cli, datafiles, tmpdir, size): }) # Create an element whose artifact is too large - create_element('target.bst', element_path, [], size) + create_element_size('target.bst', project, element_path, [], size) res = cli.run(project=project, args=['build', 'target.bst']) res.assert_main_error(ErrorDomain.STREAM, None) @@ -99,7 +79,7 @@ def test_artifact_too_large(cli, datafiles, tmpdir, size): @pytest.mark.datafiles(DATA_DIR) def test_expiry_order(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' cache_location = os.path.join(project, 'cache', 'artifacts', 'ostree') checkout = os.path.join(project, 'workspace') @@ -110,21 +90,21 @@ def test_expiry_order(cli, datafiles, tmpdir): }) # Create an artifact - create_element('dep.bst', element_path, [], 2000000) + create_element_size('dep.bst', project, element_path, [], 2000000) res = cli.run(project=project, args=['build', 'dep.bst']) res.assert_success() # Create another artifact - create_element('unrelated.bst', element_path, [], 2000000) + create_element_size('unrelated.bst', project, element_path, [], 2000000) res = cli.run(project=project, args=['build', 'unrelated.bst']) res.assert_success() # And build something else - create_element('target.bst', element_path, [], 2000000) + create_element_size('target.bst', project, element_path, [], 2000000) res = cli.run(project=project, args=['build', 'target.bst']) res.assert_success() - create_element('target2.bst', element_path, [], 2000000) + create_element_size('target2.bst', project, element_path, [], 2000000) res = cli.run(project=project, args=['build', 'target2.bst']) res.assert_success() @@ -133,7 +113,7 @@ def test_expiry_order(cli, datafiles, tmpdir): res.assert_success() # Finally, build something that will cause the cache to overflow - create_element('expire.bst', element_path, [], 2000000) + create_element_size('expire.bst', project, element_path, [], 2000000) res = cli.run(project=project, args=['build', 'expire.bst']) res.assert_success() @@ -153,7 +133,7 @@ def test_expiry_order(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_keep_dependencies(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' cache_location = os.path.join(project, 'cache', 'artifacts', 'ostree') cli.configure({ @@ -163,12 +143,12 @@ def test_keep_dependencies(cli, datafiles, tmpdir): }) # Create a pretty big dependency - create_element('dependency.bst', element_path, [], 5000000) + create_element_size('dependency.bst', project, element_path, [], 5000000) res = cli.run(project=project, args=['build', 'dependency.bst']) res.assert_success() # Now create some other unrelated artifact - create_element('unrelated.bst', element_path, [], 4000000) + create_element_size('unrelated.bst', project, element_path, [], 4000000) res = cli.run(project=project, args=['build', 'unrelated.bst']) res.assert_success() @@ -184,7 +164,8 @@ def test_keep_dependencies(cli, datafiles, tmpdir): # duplicating artifacts (bad!) we need to make this equal in size # or smaller than half the size of its dependencies. # - create_element('target.bst', element_path, ['dependency.bst'], 2000000) + create_element_size('target.bst', project, + element_path, ['dependency.bst'], 2000000) res = cli.run(project=project, args=['build', 'target.bst']) res.assert_success() @@ -197,7 +178,7 @@ def test_keep_dependencies(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_never_delete_dependencies(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' cli.configure({ 'cache': { @@ -206,10 +187,14 @@ def test_never_delete_dependencies(cli, datafiles, tmpdir): }) # Create a build tree - create_element('dependency.bst', element_path, [], 8000000) - create_element('related.bst', element_path, ['dependency.bst'], 8000000) - create_element('target.bst', element_path, ['related.bst'], 8000000) - create_element('target2.bst', element_path, ['target.bst'], 8000000) + create_element_size('dependency.bst', project, + element_path, [], 8000000) + create_element_size('related.bst', project, + element_path, ['dependency.bst'], 8000000) + create_element_size('target.bst', project, + element_path, ['related.bst'], 8000000) + create_element_size('target2.bst', project, + element_path, ['target.bst'], 8000000) # We try to build this pipeline, but it's too big for the # cache. Since all elements are required, the build should fail. @@ -249,7 +234,7 @@ def test_never_delete_dependencies(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_invalid_cache_quota(cli, datafiles, tmpdir, quota, success): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + os.makedirs(os.path.join(project, 'elements')) cli.configure({ 'cache': { diff --git a/tests/examples/junctions.py b/tests/examples/junctions.py index 49e2ebbff..d2a653884 100644 --- a/tests/examples/junctions.py +++ b/tests/examples/junctions.py @@ -11,42 +11,12 @@ DATA_DIR = os.path.join( os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'junctions' ) -JUNCTION_IMPORT_PATH = os.path.join( - os.path.dirname(os.path.realpath(__file__)), '..', '..', 'doc', 'examples', 'autotools' -) - - -def ammend_juntion_path_paths(tmpdir): - # The junction element in the examples/junctions project uses a local source type. - # It's "path:" must specify a relative path from the project's root directory. - # For the hello-junction element to function during these tests, the copy of the junctions - # project made in the buildstream/tmp/directory, "path:" must be ammended to be the relative - # path to the autotools example from the temporary test directory. - junction_element = os.path.join(tmpdir, "elements", "hello-junction.bst") - junction_element_bst = "" - junction_relative_path = os.path.relpath(JUNCTION_IMPORT_PATH, tmpdir) - with open(junction_element, 'r') as f: - junction_element_bst = f.read() - ammended_element_bst = junction_element_bst.replace("../autotools", junction_relative_path) - with open(junction_element, 'w') as f: - f.write(ammended_element_bst) - - -# Check that the autotools project is where the junctions example expects and -# contains the hello.bst element. -@pytest.mark.datafiles(DATA_DIR) -def test_autotools_example_is_present(datafiles): - autotools_path = JUNCTION_IMPORT_PATH - assert os.path.exists(autotools_path) - assert os.path.exists(os.path.join(autotools_path, "elements", "hello.bst")) - # Test that the project builds successfully @pytest.mark.skipif(not IS_LINUX, reason='Only available on linux') @pytest.mark.datafiles(DATA_DIR) def test_build(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) - ammend_juntion_path_paths(str(tmpdir)) result = cli.run(project=project, args=['build', 'callHello.bst']) result.assert_success() @@ -57,7 +27,6 @@ def test_build(cli, tmpdir, datafiles): @pytest.mark.datafiles(DATA_DIR) def test_shell_call_hello(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) - ammend_juntion_path_paths(str(tmpdir)) result = cli.run(project=project, args=['build', 'callHello.bst']) result.assert_success() @@ -73,7 +42,6 @@ def test_shell_call_hello(cli, tmpdir, datafiles): def test_open_cross_junction_workspace(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) workspace_dir = os.path.join(str(tmpdir), "workspace_hello_junction") - ammend_juntion_path_paths(str(tmpdir)) result = cli.run(project=project, args=['workspace', 'open', 'hello-junction.bst:hello.bst', workspace_dir]) diff --git a/tests/format/project.py b/tests/format/project.py index 9d595981b..df1a2364b 100644 --- a/tests/format/project.py +++ b/tests/format/project.py @@ -2,7 +2,7 @@ import os import pytest from buildstream import _yaml from buildstream._exceptions import ErrorDomain, LoadErrorReason -from tests.testutils.runcli import cli +from tests.testutils import cli, filetypegenerator # Project directory @@ -90,6 +90,48 @@ def test_project_unsupported(cli, datafiles): result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNSUPPORTED_PROJECT) +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'element-path')) +def test_missing_element_path_directory(cli, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + result = cli.run(project=project, args=['workspace', 'list']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.MISSING_FILE) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'element-path')) +def test_element_path_not_a_directory(cli, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + path = os.path.join(project, 'elements') + for file_type in filetypegenerator.generate_file_types(path): + result = cli.run(project=project, args=['workspace', 'list']) + if not os.path.isdir(path): + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID_KIND) + else: + result.assert_success() + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'local-plugin')) +def test_missing_local_plugin_directory(cli, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + result = cli.run(project=project, args=['workspace', 'list']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.MISSING_FILE) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'local-plugin')) +def test_local_plugin_not_directory(cli, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + path = os.path.join(project, 'plugins') + for file_type in filetypegenerator.generate_file_types(path): + result = cli.run(project=project, args=['workspace', 'list']) + if not os.path.isdir(path): + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID_KIND) + else: + result.assert_success() + + @pytest.mark.datafiles(DATA_DIR) def test_project_plugin_load_allowed(cli, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename, 'plugin-allowed') diff --git a/tests/format/project/element-path/project.conf b/tests/format/project/element-path/project.conf new file mode 100644 index 000000000..57e87de4f --- /dev/null +++ b/tests/format/project/element-path/project.conf @@ -0,0 +1,2 @@ +name: foo +element-path: elements diff --git a/tests/format/project/local-plugin/project.conf b/tests/format/project/local-plugin/project.conf new file mode 100644 index 000000000..97166e350 --- /dev/null +++ b/tests/format/project/local-plugin/project.conf @@ -0,0 +1,6 @@ +name: foo +plugins: +- origin: local + path: plugins + sources: + mysource: 0 diff --git a/tests/frontend/push.py b/tests/frontend/push.py index 471991ef7..be324ca53 100644 --- a/tests/frontend/push.py +++ b/tests/frontend/push.py @@ -202,7 +202,7 @@ def test_push_after_pull(cli, tmpdir, datafiles): @pytest.mark.datafiles(DATA_DIR) def test_artifact_expires(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' # Create an artifact share (remote artifact cache) in the tmpdir/artifactshare # Mock a file system with 12 MB free disk space @@ -215,12 +215,12 @@ def test_artifact_expires(cli, datafiles, tmpdir): }) # Create and build an element of 5 MB - create_element_size('element1.bst', element_path, [], int(5e6)) # [] => no deps + create_element_size('element1.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element1.bst']) result.assert_success() # Create and build an element of 5 MB - create_element_size('element2.bst', element_path, [], int(5e6)) # [] => no deps + create_element_size('element2.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element2.bst']) result.assert_success() @@ -231,7 +231,7 @@ def test_artifact_expires(cli, datafiles, tmpdir): assert_shared(cli, share, project, 'element2.bst') # Create and build another element of 5 MB (This will exceed the free disk space available) - create_element_size('element3.bst', element_path, [], int(5e6)) + create_element_size('element3.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element3.bst']) result.assert_success() @@ -250,7 +250,7 @@ def test_artifact_expires(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_artifact_too_large(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' # Create an artifact share (remote cache) in tmpdir/artifactshare # Mock a file system with 5 MB total space @@ -263,12 +263,12 @@ def test_artifact_too_large(cli, datafiles, tmpdir): }) # Create and push a 3MB element - create_element_size('small_element.bst', element_path, [], int(3e6)) + create_element_size('small_element.bst', project, element_path, [], int(3e6)) result = cli.run(project=project, args=['build', 'small_element.bst']) result.assert_success() # Create and try to push a 6MB element. - create_element_size('large_element.bst', element_path, [], int(6e6)) + create_element_size('large_element.bst', project, element_path, [], int(6e6)) result = cli.run(project=project, args=['build', 'large_element.bst']) result.assert_success() @@ -285,7 +285,7 @@ def test_artifact_too_large(cli, datafiles, tmpdir): @pytest.mark.datafiles(DATA_DIR) def test_recently_pulled_artifact_does_not_expire(cli, datafiles, tmpdir): project = os.path.join(datafiles.dirname, datafiles.basename) - element_path = os.path.join(project, 'elements') + element_path = 'elements' # Create an artifact share (remote cache) in tmpdir/artifactshare # Mock a file system with 12 MB free disk space @@ -298,11 +298,11 @@ def test_recently_pulled_artifact_does_not_expire(cli, datafiles, tmpdir): }) # Create and build 2 elements, each of 5 MB. - create_element_size('element1.bst', element_path, [], int(5e6)) + create_element_size('element1.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element1.bst']) result.assert_success() - create_element_size('element2.bst', element_path, [], int(5e6)) + create_element_size('element2.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element2.bst']) result.assert_success() @@ -327,7 +327,7 @@ def test_recently_pulled_artifact_does_not_expire(cli, datafiles, tmpdir): assert cli.get_element_state(project, 'element1.bst') == 'cached' # Create and build the element3 (of 5 MB) - create_element_size('element3.bst', element_path, [], int(5e6)) + create_element_size('element3.bst', project, element_path, [], int(5e6)) result = cli.run(project=project, args=['build', 'element3.bst']) result.assert_success() diff --git a/tests/sources/local.py b/tests/sources/local.py index 9dfb5f972..de12473d9 100644 --- a/tests/sources/local.py +++ b/tests/sources/local.py @@ -1,8 +1,8 @@ import os import pytest -from buildstream._exceptions import ErrorDomain -from tests.testutils import cli +from buildstream._exceptions import ErrorDomain, LoadErrorReason +from tests.testutils import cli, filetypegenerator DATA_DIR = os.path.join( os.path.dirname(os.path.realpath(__file__)), @@ -11,17 +11,62 @@ DATA_DIR = os.path.join( @pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) -def test_missing_file(cli, tmpdir, datafiles): +def test_missing_path(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) # Removing the local file causes preflight to fail - localfile = os.path.join(datafiles.dirname, datafiles.basename, 'file.txt') + localfile = os.path.join(project, 'file.txt') os.remove(localfile) result = cli.run(project=project, args=[ 'show', 'target.bst' ]) - result.assert_main_error(ErrorDomain.SOURCE, None) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.MISSING_FILE) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) +def test_non_regular_file_or_directory(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + localfile = os.path.join(project, 'file.txt') + + for file_type in filetypegenerator.generate_file_types(localfile): + result = cli.run(project=project, args=[ + 'show', 'target.bst' + ]) + if os.path.isdir(localfile) and not os.path.islink(localfile): + result.assert_success() + elif os.path.isfile(localfile) and not os.path.islink(localfile): + result.assert_success() + else: + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID_KIND) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) +def test_invalid_absolute_path(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + + with open(os.path.join(project, "target.bst"), 'r') as f: + old_yaml = f.read() + + new_yaml = old_yaml.replace("file.txt", os.path.join(project, "file.txt")) + assert old_yaml != new_yaml + + with open(os.path.join(project, "target.bst"), 'w') as f: + f.write(new_yaml) + + result = cli.run(project=project, args=['show', 'target.bst']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'invalid-relative-path')) +def test_invalid_relative_path(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + + result = cli.run(project=project, args=['show', 'target.bst']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID) @pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) diff --git a/tests/sources/local/invalid-relative-path/file.txt b/tests/sources/local/invalid-relative-path/file.txt new file mode 100644 index 000000000..a496efee8 --- /dev/null +++ b/tests/sources/local/invalid-relative-path/file.txt @@ -0,0 +1 @@ +This is a text file diff --git a/tests/sources/local/invalid-relative-path/project.conf b/tests/sources/local/invalid-relative-path/project.conf new file mode 100644 index 000000000..afa0f5475 --- /dev/null +++ b/tests/sources/local/invalid-relative-path/project.conf @@ -0,0 +1,2 @@ +# Basic project +name: foo diff --git a/tests/sources/local/invalid-relative-path/target.bst b/tests/sources/local/invalid-relative-path/target.bst new file mode 100644 index 000000000..b09f180e8 --- /dev/null +++ b/tests/sources/local/invalid-relative-path/target.bst @@ -0,0 +1,5 @@ +kind: import +description: This is the pony +sources: +- kind: local + path: ../invalid-relative-path/file.txt diff --git a/tests/sources/patch.py b/tests/sources/patch.py index 697a0ccfb..39d43369d 100644 --- a/tests/sources/patch.py +++ b/tests/sources/patch.py @@ -1,8 +1,8 @@ import os import pytest -from buildstream._exceptions import ErrorDomain -from tests.testutils import cli +from buildstream._exceptions import ErrorDomain, LoadErrorReason +from tests.testutils import cli, filetypegenerator DATA_DIR = os.path.join( os.path.dirname(os.path.realpath(__file__)), @@ -15,27 +15,56 @@ def test_missing_patch(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) # Removing the local file causes preflight to fail - localfile = os.path.join(datafiles.dirname, datafiles.basename, 'file_1.patch') + localfile = os.path.join(project, 'file_1.patch') os.remove(localfile) result = cli.run(project=project, args=[ 'show', 'target.bst' ]) - result.assert_main_error(ErrorDomain.SOURCE, 'patch-no-exist') + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.MISSING_FILE) @pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) def test_non_regular_file_patch(cli, tmpdir, datafiles): project = os.path.join(datafiles.dirname, datafiles.basename) - # Add a fifo, that's not a regular file, should cause explosions - patch_path = os.path.join(datafiles.dirname, datafiles.basename, 'irregular_file.patch') - os.mkfifo(patch_path) + patch_path = os.path.join(project, 'irregular_file.patch') + for file_type in filetypegenerator.generate_file_types(patch_path): + result = cli.run(project=project, args=[ + 'show', 'irregular.bst' + ]) + if os.path.isfile(patch_path) and not os.path.islink(patch_path): + result.assert_success() + else: + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID_KIND) - result = cli.run(project=project, args=[ - 'show', 'irregular.bst' - ]) - result.assert_main_error(ErrorDomain.SOURCE, "patch-not-a-file") + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) +def test_invalid_absolute_path(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + + with open(os.path.join(project, "target.bst"), 'r') as f: + old_yaml = f.read() + new_yaml = old_yaml.replace("file_1.patch", + os.path.join(project, "file_1.patch")) + assert old_yaml != new_yaml + + with open(os.path.join(project, "target.bst"), 'w') as f: + f.write(new_yaml) + + result = cli.run(project=project, args=['show', 'target.bst']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID) + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, 'invalid-relative-path')) +def test_invalid_relative_path(cli, tmpdir, datafiles): + project = os.path.join(datafiles.dirname, datafiles.basename) + + result = cli.run(project=project, args=['show', 'irregular.bst']) + result.assert_main_error(ErrorDomain.LOAD, + LoadErrorReason.PROJ_PATH_INVALID) @pytest.mark.datafiles(os.path.join(DATA_DIR, 'basic')) diff --git a/tests/sources/patch/invalid-relative-path/file_1.patch b/tests/sources/patch/invalid-relative-path/file_1.patch new file mode 100644 index 000000000..424a486dd --- /dev/null +++ b/tests/sources/patch/invalid-relative-path/file_1.patch @@ -0,0 +1,7 @@ +diff --git a/file.txt b/file.txt +index a496efe..341ef26 100644 +--- a/file.txt ++++ b/file.txt +@@ -1 +1 @@ +-This is a text file ++This is text file with superpowers diff --git a/tests/sources/patch/invalid-relative-path/irregular.bst b/tests/sources/patch/invalid-relative-path/irregular.bst new file mode 100644 index 000000000..6b63a4edb --- /dev/null +++ b/tests/sources/patch/invalid-relative-path/irregular.bst @@ -0,0 +1,5 @@ +kind: import +description: This is the pony +sources: +- kind: patch + path: ../invalid-relative-path/irregular_file.patch diff --git a/tests/sources/patch/invalid-relative-path/project.conf b/tests/sources/patch/invalid-relative-path/project.conf new file mode 100644 index 000000000..afa0f5475 --- /dev/null +++ b/tests/sources/patch/invalid-relative-path/project.conf @@ -0,0 +1,2 @@ +# Basic project +name: foo diff --git a/tests/testutils/element_generators.py b/tests/testutils/element_generators.py index 3f6090da8..49f235c61 100644 --- a/tests/testutils/element_generators.py +++ b/tests/testutils/element_generators.py @@ -18,11 +18,12 @@ from buildstream import _yaml # Returns: # Nothing (creates a .bst file of specified size) # -def create_element_size(name, path, dependencies, size): - os.makedirs(path, exist_ok=True) +def create_element_size(name, project_dir, elements_path, dependencies, size): + full_elements_path = os.path.join(project_dir, elements_path) + os.makedirs(full_elements_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: + with open(os.path.join(project_dir, name + '_data'), 'wb+') as f: f.write(os.urandom(size)) # Simplest case: We want this file (of specified size) to just @@ -32,9 +33,9 @@ def create_element_size(name, path, dependencies, size): 'sources': [ { 'kind': 'local', - 'path': os.path.join(path, name + '_data') + 'path': name + '_data' } ], 'depends': dependencies } - _yaml.dump(element, os.path.join(path, name)) + _yaml.dump(element, os.path.join(project_dir, elements_path, name)) diff --git a/tests/testutils/filetypegenerator.py b/tests/testutils/filetypegenerator.py new file mode 100644 index 000000000..2dadb7e80 --- /dev/null +++ b/tests/testutils/filetypegenerator.py @@ -0,0 +1,62 @@ +# +# Copyright (C) 2018 Codethink Limited +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Tiago Gomes <tiago.gomes@codethink.co.uk> + +import os +import socket + + +# generate_file_types() +# +# Generator that creates a regular file directory, symbolic link, fifo +# and socket at the specified path. +# +# Args: +# path: (str) path where to create each different type of file +# +def generate_file_types(path): + def clean(): + if os.path.exists(path): + if os.path.isdir(path): + os.rmdir(path) + else: + os.remove(path) + + clean() + + with open(path, 'w') as f: + pass + yield + clean() + + os.makedirs(path) + yield + clean() + + os.symlink("project.conf", path) + yield + clean() + + os.mkfifo(path) + yield + clean() + + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.bind(path) + yield + clean() |