# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-## Copyright 2021-2024 Canonical Ltd.## This program is free software; you can redistribute it and/or# modify it under the terms of the GNU Lesser General Public# License version 3 as published by the Free Software Foundation.## This program 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 program. If not, see <http://www.gnu.org/licenses/>."""Unit tests for partition utilities."""importrefromcollections.abcimportIterable,SequencefrompathlibimportPathfromcraft_partsimporterrors,featuresVALID_PARTITION_REGEX=re.compile(r"(?!-)[a-z0-9-]+(?<!-)",re.ASCII)VALID_NAMESPACE_REGEX=re.compile(r"[a-z0-9]+",re.ASCII)VALID_NAMESPACED_PARTITION_REGEX=re.compile(VALID_NAMESPACE_REGEX.pattern+r"/"+VALID_PARTITION_REGEX.pattern,re.ASCII)PARTITION_INVALID_MSG=("Partitions must only contain lowercase letters, numbers,""and hyphens, and may not begin or end with a hyphen.")
[docs]defvalidate_partition_names(partitions:Sequence[str]|None)->None:"""Validate the partition feature set. If the partition feature is enabled, then: - the first partition must be "default" - each partition name must contain only lowercase alphanumeric characters and hyphens, but not begin or end with a hyphen - partitions are unique Namespaced partitions can also be validated in addition to regular (or 'non-namespaced') partitions. The format is `<namespace>/<partition>`. Namespaced partition names follow the same conventions described above. Namespace names must consist of only lowercase alphanumeric characters. :param partitions: Partition data to verify. :raises ValueError: If the partitions are not valid or the feature is not enabled. """ifnotfeatures.Features().enable_partitions:ifpartitions:raiseerrors.FeatureError("Partitions are defined but partition feature is not enabled.")returnifnotpartitions:raiseerrors.FeatureError("Partition feature is enabled but no partitions are defined.")ifpartitions[0]!="default":raiseerrors.FeatureError("First partition must be 'default'.")iflen(partitions)!=len(set(partitions)):raiseerrors.FeatureError("Partitions must be unique.")_validate_partition_naming_convention(partitions)_validate_namespace_conflicts(partitions)
def_is_valid_partition_name(partition:str)->bool:"""Check if a partition name is valid. :param partition: partition to check :returns: true if the namespaced partition is valid """returnbool(re.fullmatch(VALID_PARTITION_REGEX,partition))def_is_valid_namespaced_partition_name(partition:str)->bool:"""Check if a namespaced partition name is valid. :param partition: partition to check :returns: true if the namespaced partition is valid """returnbool(re.fullmatch(VALID_NAMESPACED_PARTITION_REGEX,partition))def_validate_partition_naming_convention(partitions:Sequence[str])->None:"""Validate naming convention of a sequence of partitions. :param partitions: Sequence of partitions to validate. :raises FeatureError: if a partition name is not valid """forpartitioninpartitions:if_is_valid_partition_name(partition)or_is_valid_namespaced_partition_name(partition):continueif"/"inpartition:raiseerrors.FeatureError(message=f"Namespaced partition {partition!r} is invalid.",details=("Namespaced partitions are formatted as `<namespace>/""<partition>`. Namespaces must only contain lowercase letters ""and numbers. "+PARTITION_INVALID_MSG),)raiseerrors.FeatureError(message=f"Partition {partition!r} is invalid.",details=PARTITION_INVALID_MSG,)def_validate_namespace_conflicts(partitions:Sequence[str])->None:"""Validate conflicts between regular partitions and namespaces. For example, `foo` conflicts in ['default', 'foo', 'foo/bar']. Assumes partition names are valid. :raises FeatureError: for namespace conflicts """namespaced_partitions:set[str]=set()regular_partitions:set[str]=set()# sort partitionsforpartitioninpartitions:if_is_valid_partition_name(partition):regular_partitions.add(partition)else:namespaced_partitions.add(partition)forregular_partitioninregular_partitions:fornamespaced_partitioninnamespaced_partitions:ifnamespaced_partition.startswith(regular_partition+"/"):raiseerrors.FeatureError(f"Partition {regular_partition!r} conflicts with the namespace of "f"partition {namespaced_partition!r}")# At this point we know that any remaining conflicts will be overlaps# caused by hyphens and namespaces. For example, "foo-bar" and "foo/bar"# would both result in environment variable FOO_BAR.underscored_partitions={}forpartitioninpartitions:underscored=partition.replace("-","_").replace("/","_")ifunderscorednotinunderscored_partitions:underscored_partitions[underscored]=partitioncontinue# Collision. Figure out which is which so we can raise a good error message.namespaced_partition=underscored_partitions[underscored]hyphenated_partition=partitionif"/"inpartition:namespaced_partition=partitionhyphenated_partition=underscored_partitions[underscored]raiseerrors.FeatureError(f"Namespaced partition {namespaced_partition!r} conflicts with hyphenated "f"partition {hyphenated_partition!r}.")
[docs]defget_partition_dir_map(base_dir:Path,partitions:Iterable[str]|None,suffix:str="")->dict[str|None,Path]:"""Return a mapping of partition directories. The default partition maps to directories in the base_dir. All other partitions map to directories in `partitions/<partition-name>`. If no partitions are provided, return a mapping of `None` to `base_dir/suffix`. :param base_dir: Base directory. :param partitions: An iterable of partition names. :param suffix: String containing the subdirectory to map to inside each partition. :returns: A mapping of partition names to paths. """ifpartitions:return{"default":base_dir/suffix,**{partition:base_dir/"partitions"/partition/suffixforpartitioninpartitionsifpartition!="default"},}return{None:base_dir/suffix}