From e479787b9d5fbe4b06264c2bd325d42ce7a0540a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Billeter?= Date: Fri, 19 Dec 2025 11:13:37 +0100 Subject: [PATCH 1/5] source.py: Allow source provenance info to be overridden This allows multi-source source plugins to provide this information per source rather than as a singular top level. This is done by adding a `provenance_node` parameter to `create_source_info()` that when specified overrides the use of the source's top level source provenance info. Co-authored-by: Joshua Zivkovic --- src/buildstream/source.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/buildstream/source.py b/src/buildstream/source.py index 0ad4b612b..44986290e 100644 --- a/src/buildstream/source.py +++ b/src/buildstream/source.py @@ -759,6 +759,14 @@ class Source(Plugin): # The defaults from the project __defaults: Optional[Dict[str, Any]] = None + BST_CUSTOM_SOURCE_PROVENANCE = False + """Whether multiple sources' provenance information are provided + + Used primarily to override the usage of top-level source + provenance of a source where individual sub-source's + provenance should instead be provided + """ + BST_REQUIRES_PREVIOUS_SOURCES_TRACK = False """Whether access to previous sources is required during track @@ -828,6 +836,12 @@ def __init__( _SourceProvenance ] = meta.provenance # The _SourceProvenance for general user provided SourceInfo + if self.__provenance is not None and self.BST_CUSTOM_SOURCE_PROVENANCE: + raise SourceError( + f"{self._get_provenance()} Custom source provenance plugin: Refusing to use top level source provenance", + reason="top-level-provenance-on-custom-implementation", + ) + self.__key = None # Cache key for source # The alias_override is only set on a re-instantiated Source @@ -1368,6 +1382,7 @@ def create_source_info( *, version_guess: Optional[str] = None, extra_data: Optional[Dict[str, str]] = None, + provenance_node: Optional[MappingNode] = None, ) -> SourceInfo: """Create a :class:`.SourceInfo` object @@ -1387,14 +1402,22 @@ def create_source_info( version: A string which represents a unique version of this source input version_guess: An optional string representing the guessed human readable version extra_data: Additional plugin defined key/values + provenance_node: An optional :class:`Node ` with source provenance attributes, + defaults to the provenance specified at the top level of the source. *Since: 2.5* """ homepage = None issue_tracker = None - if self.__provenance is not None: - homepage = self.__provenance.homepage - issue_tracker = self.__provenance.issue_tracker + + if provenance_node is not None: + source_provenance: Optional[_SourceProvenance] = _SourceProvenance.new_from_node(provenance_node) + else: + source_provenance = self.__provenance + + if source_provenance is not None: + homepage = source_provenance.homepage + issue_tracker = source_provenance.issue_tracker return SourceInfo( self.get_kind(), From 0cb0e5eaaa64a06d8658c68ef84ce623f9518dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Billeter?= Date: Fri, 9 Jan 2026 16:37:15 +0100 Subject: [PATCH 2/5] tests/frontend/show.py: Add source info test for multi-source plugin --- tests/frontend/show.py | 29 +++++++++++ .../source-info/elements/multisource.bst | 4 ++ .../source-info/plugins/multisource.py | 48 +++++++++++++++++++ tests/frontend/source-info/project.conf | 1 + 4 files changed, 82 insertions(+) create mode 100644 tests/frontend/source-info/elements/multisource.bst create mode 100644 tests/frontend/source-info/plugins/multisource.py diff --git a/tests/frontend/show.py b/tests/frontend/show.py index 6ad762224..62d33cd1f 100644 --- a/tests/frontend/show.py +++ b/tests/frontend/show.py @@ -814,3 +814,32 @@ def test_source_info_workspace(cli, datafiles, tmpdir): # There is no version guessing for a workspace assert source_info.get_str("version-guess", None) is None + + +@pytest.mark.datafiles(os.path.join(DATA_DIR, "source-info")) +def test_multi_source_info(cli, datafiles): + project = str(datafiles) + result = cli.run( + project=project, silent=True, args=["show", "--format", "%{name}:\n%{source-info}", "multisource.bst"] + ) + result.assert_success() + + loaded = _yaml.load_data(result.output) + sources = loaded.get_sequence("multisource.bst") + + source_info = sources.mapping_at(0) + assert source_info.get_str("kind") == "multisource" + assert source_info.get_str("url") == "http://ponyfarm.com/ponies" + assert source_info.get_str("medium") == "pony-ride" + assert source_info.get_str("version-type") == "pony-age" + assert source_info.get_str("version") == "1234567" + assert source_info.get_str("version-guess", None) == "12" + + source_info = sources.mapping_at(1) + assert source_info.get_str("kind") == "multisource" + assert source_info.get_str("url") == "http://ponyfarm.com/happy" + assert source_info.get_str("medium") == "pony-ride" + assert source_info.get_str("version-type") == "pony-age" + assert source_info.get_str("version") == "1234567" + assert source_info.get_str("version-guess", None) == "12" + assert source_info.get_str("homepage") == "http://happy.ponyfarm.com" diff --git a/tests/frontend/source-info/elements/multisource.bst b/tests/frontend/source-info/elements/multisource.bst new file mode 100644 index 000000000..a04dee3bc --- /dev/null +++ b/tests/frontend/source-info/elements/multisource.bst @@ -0,0 +1,4 @@ +kind: import + +sources: +- kind: multisource diff --git a/tests/frontend/source-info/plugins/multisource.py b/tests/frontend/source-info/plugins/multisource.py new file mode 100644 index 000000000..774e92d4d --- /dev/null +++ b/tests/frontend/source-info/plugins/multisource.py @@ -0,0 +1,48 @@ +from buildstream import Node, Source + + +class MultiSource(Source): + BST_MIN_VERSION = "2.0" + + BST_CUSTOM_SOURCE_PROVENANCE = True + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + def load_ref(self, node): + pass + + def get_ref(self): + return {} + + def set_ref(self, ref, node): + pass + + def is_cached(self): + return False + + def collect_source_info(self): + return [ + self.create_source_info( + "http://ponyfarm.com/ponies", "pony-ride", "pony-age", "1234567", version_guess="12" + ), + self.create_source_info( + "http://ponyfarm.com/happy", + "pony-ride", + "pony-age", + "1234567", + version_guess="12", + provenance_node=Node.from_dict({"homepage": "http://happy.ponyfarm.com"}), + ), + ] + + +# Plugin entry point +def setup(): + return MultiSource diff --git a/tests/frontend/source-info/project.conf b/tests/frontend/source-info/project.conf index 86cfd087b..3ba58ad6f 100644 --- a/tests/frontend/source-info/project.conf +++ b/tests/frontend/source-info/project.conf @@ -12,5 +12,6 @@ plugins: path: plugins sources: - extradata + - multisource - testsource - unimplemented From 8409e5d62a2616067978ee029bd17769203112a0 Mon Sep 17 00:00:00 2001 From: Joshua Zivkovic Date: Fri, 16 Jan 2026 15:55:46 +0000 Subject: [PATCH 3/5] Add source provenance attribute tests --- tests/sources/source_provenance_attributes.py | 51 +++++++++++++++++++ .../elements/target.bst | 7 +++ .../source_provenance_attributes/files/file | 1 + .../plugins/multisource-plugin.py | 36 +++++++++++++ .../source_provenance_attributes/project.conf | 14 +++++ 5 files changed, 109 insertions(+) create mode 100644 tests/sources/source_provenance_attributes.py create mode 100644 tests/sources/source_provenance_attributes/elements/target.bst create mode 100644 tests/sources/source_provenance_attributes/files/file create mode 100644 tests/sources/source_provenance_attributes/plugins/multisource-plugin.py create mode 100644 tests/sources/source_provenance_attributes/project.conf diff --git a/tests/sources/source_provenance_attributes.py b/tests/sources/source_provenance_attributes.py new file mode 100644 index 000000000..65f28740a --- /dev/null +++ b/tests/sources/source_provenance_attributes.py @@ -0,0 +1,51 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Pylint doesn't play well with fixtures and dependency injection from pytest +# pylint: disable=redefined-outer-name + +import os +import pytest + +from buildstream._testing import generate_project, load_yaml +from buildstream._testing import cli # pylint: disable=unused-import +from buildstream.exceptions import ErrorDomain + + +DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "source_provenance_attributes") + + +################################################################## +# Tests # +################################################################## +# Test that no defined source provenance attributes blocks all source provenance data +@pytest.mark.datafiles(DATA_DIR) +def test_source_provenance_disallow_top_level(cli, datafiles): + project = str(datafiles) + + # Set the project_dir alias in project.conf to the path to the tested project + project_config_path = os.path.join(project, "project.conf") + project_config = load_yaml(project_config_path) + aliases = project_config.get_mapping("aliases") + aliases["project_dir"] = "file://{}".format(project) + + generate_project(project, project_config) + + # Make sure disallowed usage of top-level source proveance fails + result = cli.run( + project=project, + args=["show", "target.bst"], + ) + + result.assert_main_error(ErrorDomain.SOURCE, "top-level-provenance-on-custom-implementation") diff --git a/tests/sources/source_provenance_attributes/elements/target.bst b/tests/sources/source_provenance_attributes/elements/target.bst new file mode 100644 index 000000000..16a82ddf2 --- /dev/null +++ b/tests/sources/source_provenance_attributes/elements/target.bst @@ -0,0 +1,7 @@ +kind: import + +sources: +- kind: multisource-plugin + url: project_dir:/files/file + provenance: + homepage: bar diff --git a/tests/sources/source_provenance_attributes/files/file b/tests/sources/source_provenance_attributes/files/file new file mode 100644 index 000000000..980a0d5f1 --- /dev/null +++ b/tests/sources/source_provenance_attributes/files/file @@ -0,0 +1 @@ +Hello World! diff --git a/tests/sources/source_provenance_attributes/plugins/multisource-plugin.py b/tests/sources/source_provenance_attributes/plugins/multisource-plugin.py new file mode 100644 index 000000000..6934eb923 --- /dev/null +++ b/tests/sources/source_provenance_attributes/plugins/multisource-plugin.py @@ -0,0 +1,36 @@ +from buildstream import Node, Source + + +class MultiSource(Source): + BST_MIN_VERSION = "2.0" + + BST_CUSTOM_SOURCE_PROVENANCE = True + + def configure(self, node): + pass + + def preflight(self): + pass + + def get_unique_key(self): + return {} + + def load_ref(self, node): + pass + + def get_ref(self): + return {} + + def set_ref(self, ref, node): + pass + + def is_cached(self): + return False + + def collect_source_info(self): + return [] + + +# Plugin entry point +def setup(): + return MultiSource diff --git a/tests/sources/source_provenance_attributes/project.conf b/tests/sources/source_provenance_attributes/project.conf new file mode 100644 index 000000000..e102c70cc --- /dev/null +++ b/tests/sources/source_provenance_attributes/project.conf @@ -0,0 +1,14 @@ +# Project with source provenance attributes +name: foo +min-version: 2.0 + +element-path: elements + +aliases: + project_dir: file://{project_dir} + +plugins: +- origin: local + path: plugins + sources: + - multisource-plugin From 01e55e87c2f530337b81deffc4fb9544039b3445 Mon Sep 17 00:00:00 2001 From: Joshua Zivkovic Date: Fri, 16 Jan 2026 15:55:09 +0000 Subject: [PATCH 4/5] Make source provenance generic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove harcoded SPDX attributes and make them be generic instead. Project allowed attributes are configured via the project config, these supported values a determined by buildstream-sbom's support Co-authored-by: Jürg Billeter Co-authored-by: Joshua Zivkovic --- src/buildstream/_project.py | 12 ++++ src/buildstream/data/projectconfig.yaml | 6 ++ src/buildstream/element.py | 27 ++++++-- src/buildstream/exceptions.py | 5 ++ src/buildstream/node.pyi | 1 + src/buildstream/source.py | 89 ++++++++++++++++++------- src/buildstream/types.py | 36 ---------- 7 files changed, 108 insertions(+), 68 deletions(-) diff --git a/src/buildstream/_project.py b/src/buildstream/_project.py index 334462df0..0af9b132b 100644 --- a/src/buildstream/_project.py +++ b/src/buildstream/_project.py @@ -125,6 +125,10 @@ def __init__( self.sandbox: Optional[MappingNode] = None self.splits: Optional[MappingNode] = None + self.source_provenance_attributes: Optional[ + MappingNode + ] = None # Source provenance attributes and their description + # # Private members # @@ -726,6 +730,7 @@ def _validate_toplevel_node(self, node, *, first_pass=False): "sources", "source-caches", "junctions", + "source-provenance-attributes", "(@)", "(?)", ] @@ -1006,6 +1011,13 @@ def _load_second_pass(self): self._shell_host_files.append(mount) + # We don't want to combine the source provenance attributes that a project defines with the defaults + # If the project config defines source-provenance-attributes use that, otherwise fall back to the defaults + # This is purely to maintain backwards compatibility with homepage and issue-tracker being available + self.source_provenance_attributes = project_conf_second_pass.get_mapping( + "source-provenance-attributes", None + ) or config.get_mapping("source-provenance-attributes") + # _load_pass(): # # Loads parts of the project configuration that are different diff --git a/src/buildstream/data/projectconfig.yaml b/src/buildstream/data/projectconfig.yaml index 4632d7a1d..f695428ca 100644 --- a/src/buildstream/data/projectconfig.yaml +++ b/src/buildstream/data/projectconfig.yaml @@ -175,6 +175,12 @@ shell: # command: [ 'sh', '-i' ] +# Define the set of fields accepted in `provenance` dictionaries of sources. +# +source-provenance-attributes: + homepage: "The project homepage URL" + issue-tracker: "The project's issue tracking URL" + # Defaults for bst commands # defaults: diff --git a/src/buildstream/element.py b/src/buildstream/element.py index fa79c2b44..aede8f1ea 100644 --- a/src/buildstream/element.py +++ b/src/buildstream/element.py @@ -85,12 +85,12 @@ from . import utils from . import _cachekey from . import _site -from .node import Node +from .node import Node, MappingNode, ScalarNode from .plugin import Plugin from .sandbox import _SandboxFlags, SandboxCommandError from .sandbox._config import SandboxConfig from .sandbox._sandboxremote import SandboxRemote -from .types import _Scope, _CacheBuildTrees, _KeyStrength, OverlapAction, _DisplayKey, _SourceProvenance +from .types import _Scope, _CacheBuildTrees, _KeyStrength, OverlapAction, _DisplayKey from ._artifact import Artifact from ._elementproxy import ElementProxy from ._elementsources import ElementSources @@ -102,7 +102,7 @@ if TYPE_CHECKING: from typing import Tuple - from .node import MappingNode, ScalarNode, SequenceNode + from .node import SequenceNode from .types import SourceRef # pylint: disable=cyclic-import @@ -2635,11 +2635,24 @@ def __load_sources(self, load_element): del source[Symbol.DIRECTORY] # Provenance is optional - provenance_node = source.get_mapping(Symbol.PROVENANCE, default=None) - provenance = None + provenance_node: MappingNode = source.get_mapping(Symbol.PROVENANCE, default=None) if provenance_node: del source[Symbol.PROVENANCE] - provenance = _SourceProvenance.new_from_node(provenance_node) + try: + provenance_node.validate_keys(project.source_provenance_attributes.keys()) + except LoadError as E: + raise LoadError( + f"Specified source provenance attribute not defined in project config\n {E}", + LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE, + ) + + # make sure everything is a string + for key, value in provenance_node.items(): + if not isinstance(value, ScalarNode): + raise LoadError( + f"{value}: Expected string for the value of provenance attribute '{key}'", + LoadErrorReason.INVALID_DATA, + ) meta_source = MetaSource( self.name, @@ -2647,7 +2660,7 @@ def __load_sources(self, load_element): self.get_kind(), kind.as_str(), directory, - provenance, + provenance_node, source, load_element.first_pass, ) diff --git a/src/buildstream/exceptions.py b/src/buildstream/exceptions.py index 4806218e9..418e51eda 100644 --- a/src/buildstream/exceptions.py +++ b/src/buildstream/exceptions.py @@ -155,3 +155,8 @@ class LoadErrorReason(Enum): This warning will be produced when a filename for a target contains invalid characters in its name. """ + + UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE = 29 + """ + Thee source provenance attribute specified was not defined in the project config + """ diff --git a/src/buildstream/node.pyi b/src/buildstream/node.pyi index 253ba93b1..6fcb3719e 100644 --- a/src/buildstream/node.pyi +++ b/src/buildstream/node.pyi @@ -65,6 +65,7 @@ class MappingNode(Node, Generic[TNode]): def items(self) -> Iterable[Tuple[str, Any]]: ... def safe_del(self, key: str) -> None: ... def validate_keys(self, valid_keys: List[str]): ... + def values(self) -> List[Node]: ... @overload def get_scalar(self, key: str) -> ScalarNode: ... @overload diff --git a/src/buildstream/source.py b/src/buildstream/source.py index 44986290e..45499f47d 100644 --- a/src/buildstream/source.py +++ b/src/buildstream/source.py @@ -38,15 +38,11 @@ to provide additional source provenance related metadata which will later be reported in :class:`.SourceInfo` objects. - The ``provenance`` dictionary supports the following fields: + The ``provenance`` dictionary itself does not have any specific required keys. - * Homepage - - The ``homepage`` attribute can be used to specify the project homepage URL - - * Issue Tracker - - The ``issue-tracker`` attribute can be used to specify the project's issue tracking URL + Any attribute used in the ``provenance`` dictionary of a source must be + defined in the project.conf using the ``source-provenance-attributes`` dictionary + to define the attribute and its significance. *Since: 2.5* @@ -371,16 +367,26 @@ import os from contextlib import contextmanager -from typing import Iterable, Iterator, Optional, Tuple, Dict, Any, Set, TYPE_CHECKING, Union +from typing import ( + Iterable, + Iterator, + Optional, + Tuple, + Dict, + Any, + Set, + TYPE_CHECKING, + Union, +) from dataclasses import dataclass from . import _yaml, utils -from .node import MappingNode +from .node import MappingNode, ScalarNode from .plugin import Plugin from .sourcemirror import SourceMirror -from .types import SourceRef, CoreWarnings, FastEnum, _SourceProvenance -from ._exceptions import BstError, ImplError, PluginError -from .exceptions import ErrorDomain +from .types import SourceRef, CoreWarnings, FastEnum +from ._exceptions import BstError, ImplError, PluginError, LoadError +from .exceptions import ErrorDomain, LoadErrorReason from ._loader.metasource import MetaSource from ._projectrefs import ProjectRefStorage from ._cachekey import generate_key @@ -396,6 +402,8 @@ # pylint: enable=cyclic-import +SourceProvenance = MappingNode + class SourceError(BstError): """This exception should be raised by :class:`.Source` implementations @@ -555,6 +563,7 @@ def __init__( url: str, homepage: Optional[str], issue_tracker: Optional[str], + provenance: Optional[SourceProvenance], medium: Union[SourceInfoMedium, str], version_type: Union[SourceVersionType, str], version: str, @@ -579,7 +588,12 @@ def __init__( self.issue_tracker: Optional[str] = issue_tracker """ - The project issue tracking URL + The issue tracker for the project + """ + + self.provenance = provenance + """ + The optional YAML node with source provenance attributes """ self.medium: Union[SourceInfoMedium, str] = medium @@ -642,10 +656,14 @@ def serialize(self) -> Dict[str, Union[str, Dict[str, str]]]: "url": self.url, } - if self.homepage is not None: - version_info["homepage"] = self.homepage - if self.issue_tracker is not None: - version_info["issue-tracker"] = self.issue_tracker + if self.provenance is not None: + # need to keep homepage/issue-tracker [also] at the top-level for backward compat + if (homepage := self.provenance.get_str("homepage", None)) is not None: + version_info["homepage"] = homepage + if (issue_tracker := self.provenance.get_str("issue-tracker", None)) is not None: + version_info["issue-tracker"] = issue_tracker + + version_info["provenance"] = self.provenance.strip_node_info() version_info["medium"] = medium_str version_info["version-type"] = version_type_str @@ -833,8 +851,8 @@ def __init__( self._directory = meta.directory # Staging relative directory self.__variables = variables # The variables used to resolve the source's config self.__provenance: Optional[ - _SourceProvenance - ] = meta.provenance # The _SourceProvenance for general user provided SourceInfo + SourceProvenance + ] = meta.provenance # The source provenance for general user provided SourceInfo if self.__provenance is not None and self.BST_CUSTOM_SOURCE_PROVENANCE: raise SourceError( @@ -1407,23 +1425,44 @@ def create_source_info( *Since: 2.5* """ - homepage = None - issue_tracker = None + project = self._get_project() + source_provenance: SourceProvenance | None if provenance_node is not None: - source_provenance: Optional[_SourceProvenance] = _SourceProvenance.new_from_node(provenance_node) + # Ensure provenance node keys are valid and values are all strings + try: + provenance_node.validate_keys(project.source_provenance_attributes.keys()) + except LoadError as E: + raise LoadError( + "Specified source provenance attribute not defined in project config\n {}".format(E), + LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE, + ) + + # Make sure everything is a string + for key, value in provenance_node.items(): + if not isinstance(value, ScalarNode): + raise LoadError( + f"{value}: Expected string for the value of provenance attribute '{key}'", + LoadErrorReason.INVALID_DATA, + ) + + source_provenance = provenance_node else: source_provenance = self.__provenance + homepage = None + issue_tracker = None + if source_provenance is not None: - homepage = source_provenance.homepage - issue_tracker = source_provenance.issue_tracker + homepage = source_provenance.get_str("homepage", None) + issue_tracker = source_provenance.get_str("issue-tracker", None) return SourceInfo( self.get_kind(), url, homepage, issue_tracker, + source_provenance, medium, version_type, version, diff --git a/src/buildstream/types.py b/src/buildstream/types.py index d4d5e3cdb..0cc2106b3 100644 --- a/src/buildstream/types.py +++ b/src/buildstream/types.py @@ -390,42 +390,6 @@ def new_from_node(cls, node: MappingNode) -> "_SourceMirror": return cls(name, aliases) -# _SourceProvenance() -# -# A simple object describing user provided source provenance information -# -# Args: -# homepage: The project homepage URL -# issue_tracker: The project issue reporting URL -# -class _SourceProvenance: - def __init__(self, homepage: Optional[str], issue_tracker: Optional[str]): - self.homepage: Optional[str] = homepage - self.issue_tracker: Optional[str] = issue_tracker - - # new_from_node(): - # - # Creates a _SourceProvenance() from a YAML loaded node. - # - # Args: - # node: The configuration node describing the spec. - # - # Returns: - # The described _SourceProvenance instance. - # - # Raises: - # LoadError: If the node is malformed. - # - @classmethod - def new_from_node(cls, node: MappingNode) -> "_SourceProvenance": - node.validate_keys(["homepage", "issue-tracker"]) - - homepage: Optional[str] = node.get_str("homepage", None) - issue_tracker: Optional[str] = node.get_str("issue-tracker", None) - - return cls(homepage, issue_tracker) - - ######################################## # Type aliases # ######################################## From 749602af74172cf447ebe339c710415c0207b1dc Mon Sep 17 00:00:00 2001 From: Joshua Zivkovic Date: Fri, 16 Jan 2026 15:55:46 +0000 Subject: [PATCH 5/5] Add source provenance attribute tests --- tests/sources/source_provenance_attributes.py | 110 +++++++++++++++++- .../elements/target_a.bst | 7 ++ .../elements/target_b.bst | 7 ++ .../source_provenance_attributes/project.conf | 3 + 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 tests/sources/source_provenance_attributes/elements/target_a.bst create mode 100644 tests/sources/source_provenance_attributes/elements/target_b.bst diff --git a/tests/sources/source_provenance_attributes.py b/tests/sources/source_provenance_attributes.py index 65f28740a..a51e7c883 100644 --- a/tests/sources/source_provenance_attributes.py +++ b/tests/sources/source_provenance_attributes.py @@ -20,7 +20,7 @@ from buildstream._testing import generate_project, load_yaml from buildstream._testing import cli # pylint: disable=unused-import -from buildstream.exceptions import ErrorDomain +from buildstream.exceptions import ErrorDomain, LoadErrorReason DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "source_provenance_attributes") @@ -40,6 +40,9 @@ def test_source_provenance_disallow_top_level(cli, datafiles): aliases = project_config.get_mapping("aliases") aliases["project_dir"] = "file://{}".format(project) + source_provenance_attrs = project_config.get_mapping("source-provenance-attributes") + source_provenance_attrs["homepage"] = "Testing" + generate_project(project, project_config) # Make sure disallowed usage of top-level source proveance fails @@ -49,3 +52,108 @@ def test_source_provenance_disallow_top_level(cli, datafiles): ) result.assert_main_error(ErrorDomain.SOURCE, "top-level-provenance-on-custom-implementation") + + +@pytest.mark.datafiles(DATA_DIR) +def test_source_provenance_no_defined_attributes(cli, datafiles): + project = str(datafiles) + + # Set the project_dir alias in project.conf to the path to the tested project + project_config_path = os.path.join(project, "project.conf") + project_config = load_yaml(project_config_path) + aliases = project_config.get_mapping("aliases") + aliases["project_dir"] = "file://{}".format(project) + + generate_project(project, project_config) + + # Make sure a non-default attribute fails + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_a.bst"], + ) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE) + + # Make sure a default attribute fails + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_b.bst"], + ) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE) + + +# Test that no defined source provenance attributes blocks all source provenance data +@pytest.mark.datafiles(DATA_DIR) +def test_source_provenance_default_attributes(cli, datafiles): + project = str(datafiles) + + # Set the project_dir alias in project.conf to the path to the tested project + project_config_path = os.path.join(project, "project.conf") + project_config = load_yaml(project_config_path) + aliases = project_config.get_mapping("aliases") + aliases["project_dir"] = "file://{}".format(project) + + # Edit config to fallback to default source provenance attributes + project_config.safe_del("source-provenance-attributes") + + generate_project(project, project_config) + + # Make sure defined attributes are available + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_b.bst"], + ) + result.assert_success() + + provenance_result = "" + for line in result.output.split("\n"): + if "provenance:" in line or " " in line: + provenance_result += line + + assert provenance_result == " provenance: homepage: foo" + + # Make sure undefined attributes fail + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_a.bst"], + ) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE) + + +# Test that no defined source provenance attributes blocks all source provenance data +@pytest.mark.datafiles(DATA_DIR) +def test_source_provenance_project_defined_attributes(cli, datafiles): + project = str(datafiles) + + # Set the project_dir alias in project.conf to the path to the tested project + project_config_path = os.path.join(project, "project.conf") + project_config = load_yaml(project_config_path) + aliases = project_config.get_mapping("aliases") + aliases["project_dir"] = "file://{}".format(project) + + # Edit config to only use project specified source provenance attributes + + source_provenance_attrs = project_config.get_mapping("source-provenance-attributes") + source_provenance_attrs["originator"] = "Testing" + + generate_project(project, project_config) + + # Make sure defined attributes are available + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_a.bst"], + ) + result.assert_success() + + provenance_result = "" + for line in result.output.split("\n"): + if "provenance:" in line or " " in line: + provenance_result += line + + assert provenance_result == " provenance: originator: bar" + + # Make sure undefined attributes fail + result = cli.run( + project=project, + args=["show", "--format", "%{source-info}", "target_b.bst"], + ) + result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.UNDEFINED_SOURCE_PROVENANCE_ATTRIBUTE) diff --git a/tests/sources/source_provenance_attributes/elements/target_a.bst b/tests/sources/source_provenance_attributes/elements/target_a.bst new file mode 100644 index 000000000..b46719284 --- /dev/null +++ b/tests/sources/source_provenance_attributes/elements/target_a.bst @@ -0,0 +1,7 @@ +kind: import + +sources: +- kind: remote + url: project_dir:/files/file + provenance: + originator: bar diff --git a/tests/sources/source_provenance_attributes/elements/target_b.bst b/tests/sources/source_provenance_attributes/elements/target_b.bst new file mode 100644 index 000000000..0587496d6 --- /dev/null +++ b/tests/sources/source_provenance_attributes/elements/target_b.bst @@ -0,0 +1,7 @@ +kind: import + +sources: +- kind: remote + url: project_dir:/files/file + provenance: + homepage: foo diff --git a/tests/sources/source_provenance_attributes/project.conf b/tests/sources/source_provenance_attributes/project.conf index e102c70cc..179308a76 100644 --- a/tests/sources/source_provenance_attributes/project.conf +++ b/tests/sources/source_provenance_attributes/project.conf @@ -12,3 +12,6 @@ plugins: path: plugins sources: - multisource-plugin + +# Don't allow any provenance +source-provenance-attributes: {}