mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
add ability to partition tree before calculationn
This commit is contained in:
parent
f6081507c0
commit
a1db4dc8b8
@ -43,7 +43,7 @@ _shtab_ahriman_sign_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_status_update_option_strings=('-h' '--help' '-s' '--status')
|
||||
_shtab_ahriman_repo_sync_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_sync_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_tree_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
|
||||
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
|
@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2023\-08\-19" "ahriman" "Generated Python Manual"
|
||||
.TH AHRIMAN "1" "2023\-08\-26" "ahriman" "Generated Python Manual"
|
||||
.SH NAME
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
@ -43,7 +43,7 @@ allow to run ahriman as non\-ahriman user. Some actions might be unavailable
|
||||
.TP
|
||||
\fB\-\-wait\-timeout\fR \fI\,WAIT_TIMEOUT\/\fR
|
||||
wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of
|
||||
zero value, tthe application will wait infinitely
|
||||
zero value, the application will wait infinitely
|
||||
|
||||
.TP
|
||||
\fB\-V\fR, \fB\-\-version\fR
|
||||
@ -558,10 +558,15 @@ usage: ahriman repo\-sync [\-h]
|
||||
sync repository files to remote server according to current settings
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-tree'\/\fR
|
||||
usage: ahriman repo\-tree [\-h]
|
||||
usage: ahriman repo\-tree [\-h] [\-p PARTITIONS]
|
||||
|
||||
dump repository tree based on packages dependencies
|
||||
|
||||
.SH OPTIONS \fI\,'ahriman repo\-tree'\/\fR
|
||||
.TP
|
||||
\fB\-p\fR \fI\,PARTITIONS\/\fR, \fB\-\-partitions\fR \fI\,PARTITIONS\/\fR
|
||||
also divide packages by independent partitions
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-triggers'\/\fR
|
||||
usage: ahriman repo\-triggers [\-h] [trigger ...]
|
||||
|
||||
|
@ -85,7 +85,7 @@ _shtab_ahriman_options=(
|
||||
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
|
||||
{-q,--quiet}"[force disable any logging (default\: False)]"
|
||||
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]"
|
||||
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, tthe application will wait infinitely (default\: -1)]:wait_timeout:"
|
||||
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
|
||||
"(- : *)"{-V,--version}"[show program\'s version number and exit]"
|
||||
)
|
||||
|
||||
@ -415,6 +415,7 @@ _shtab_ahriman_repo_sync_options=(
|
||||
|
||||
_shtab_ahriman_repo_tree_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-p,--partitions}"[also divide packages by independent partitions (default\: 1)]:partitions:"
|
||||
)
|
||||
|
||||
_shtab_ahriman_repo_triggers_options=(
|
||||
|
@ -715,6 +715,8 @@ def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser = root.add_parser("repo-tree", help="dump repository tree",
|
||||
description="dump repository tree based on packages dependencies",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions",
|
||||
type=int, default=1)
|
||||
parser.set_defaults(handler=handlers.Structure, lock=None, report=False, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
@ -22,7 +22,7 @@ import argparse
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters import TreePrinter
|
||||
from ahriman.core.formatters import StringPrinter, TreePrinter
|
||||
from ahriman.core.tree import Tree
|
||||
|
||||
|
||||
@ -45,8 +45,14 @@ class Structure(Handler):
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
application = Application(architecture, configuration, report=report)
|
||||
packages = application.repository.packages()
|
||||
partitions = Tree.partition(application.repository.packages(), count=args.partitions)
|
||||
|
||||
tree = Tree.resolve(packages)
|
||||
for partition_id, partition in enumerate(partitions):
|
||||
StringPrinter(f"partition #{partition_id}").print(verbose=False)
|
||||
|
||||
tree = Tree.resolve(partition)
|
||||
for num, level in enumerate(tree):
|
||||
TreePrinter(num, level).print(verbose=True, separator=" ")
|
||||
|
||||
# empty line
|
||||
StringPrinter("").print(verbose=False)
|
||||
|
@ -244,6 +244,21 @@ class PasswordError(ValueError):
|
||||
ValueError.__init__(self, f"Password error: {details}")
|
||||
|
||||
|
||||
class PartitionError(RuntimeError):
|
||||
"""
|
||||
exception raised during packages partition actions
|
||||
"""
|
||||
|
||||
def __init__(self, count: int) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
count(int): count of partitions
|
||||
"""
|
||||
RuntimeError.__init__(self, f"Could not divide packages into {count} partitions")
|
||||
|
||||
|
||||
class PkgbuildGeneratorError(RuntimeError):
|
||||
"""
|
||||
exception class for support type triggers
|
||||
|
@ -38,7 +38,7 @@ class TreePrinter(StringPrinter):
|
||||
level(int): dependencies tree level
|
||||
packages(list[Package]): packages which belong to this level
|
||||
"""
|
||||
StringPrinter.__init__(self, f"level {level}")
|
||||
StringPrinter.__init__(self, f"level #{level}")
|
||||
self.packages = packages
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
|
@ -21,9 +21,10 @@ from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ahriman.core.util import partition
|
||||
from ahriman.core.exceptions import PartitionError
|
||||
from ahriman.core.util import minmax, partition
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -128,6 +129,75 @@ class Tree:
|
||||
"""
|
||||
self.leaves = leaves
|
||||
|
||||
@staticmethod
|
||||
def balance(partitions: list[list[Leaf]]) -> list[list[Leaf]]:
|
||||
"""
|
||||
balance partitions. This method tries to find the longest and the shortest lists and move free leaves between
|
||||
them if possible. In case if there are no free packages (i.e. the ones which don't depend on any other in
|
||||
partition and are not dependency of any), it will drop it as it is. This method is guaranteed to produce the
|
||||
same unsorted sequences for same unsorted input
|
||||
|
||||
Args:
|
||||
partitions(list[list[Leaf]]): source unbalanced partitions
|
||||
|
||||
Returns:
|
||||
list[list[Leaf]]: balanced partitions
|
||||
"""
|
||||
# to make sure that we will have same sequences after balance we need to ensure that list is sorted
|
||||
partitions = [
|
||||
sorted(part, key=lambda leaf: leaf.package.base)
|
||||
for part in partitions if part
|
||||
]
|
||||
|
||||
while True:
|
||||
min_part, max_part = minmax(partitions, key=len)
|
||||
if len(max_part) - len(min_part) <= 1: # there is nothing to balance
|
||||
break
|
||||
|
||||
# find first package from max list which is not dependency and doesn't depend on any other package
|
||||
free_index = next(
|
||||
(
|
||||
index
|
||||
for index, leaf in enumerate(max_part)
|
||||
if not leaf.is_dependency(max_part) and leaf.is_root(max_part)
|
||||
),
|
||||
None
|
||||
)
|
||||
if free_index is None: # impossible to balance between the shortest and the longest
|
||||
break
|
||||
|
||||
min_part.append(max_part.pop(free_index))
|
||||
|
||||
return partitions
|
||||
|
||||
@staticmethod
|
||||
def partition(packages: Iterable[Package], *, count: int) -> list[list[Package]]:
|
||||
"""
|
||||
partition tree into independent chunks of more or less equal amount of packages. The packages in produced
|
||||
partitions don't depend on any package from other partitions
|
||||
|
||||
Args:
|
||||
packages(Iterable[Package]): packages list
|
||||
count(int): maximal amount of partitions
|
||||
|
||||
Returns:
|
||||
list[list[Package]]: list of packages lists based on their dependencies. The amount of elements in each
|
||||
sublist is less or equal to ``count``
|
||||
|
||||
Raises:
|
||||
PartitionError: in case if it is impossible to divide tree by specified amount of partitions
|
||||
"""
|
||||
if count < 1:
|
||||
raise PartitionError(count)
|
||||
|
||||
# special case
|
||||
if count == 1:
|
||||
return [sorted(packages, key=lambda package: package.base)]
|
||||
|
||||
leaves = [Leaf(package) for package in packages]
|
||||
instance = Tree(leaves)
|
||||
return instance.partitions(count=count)
|
||||
|
||||
@staticmethod
|
||||
def resolve(packages: Iterable[Package]) -> list[list[Package]]:
|
||||
"""
|
||||
@ -143,6 +213,22 @@ class Tree:
|
||||
instance = Tree(leaves)
|
||||
return instance.levels()
|
||||
|
||||
@staticmethod
|
||||
def sort(leaves: list[list[Leaf]]) -> list[list[Package]]:
|
||||
"""
|
||||
sort given list of leaves by package base
|
||||
|
||||
Args:
|
||||
leaves(list[list[Leaf]]): leaves to sort
|
||||
|
||||
Returns:
|
||||
list[list[Package]]: sorted list of packages on each level
|
||||
"""
|
||||
return [
|
||||
sorted([leaf.package for leaf in level], key=lambda package: package.base)
|
||||
for level in leaves if level
|
||||
]
|
||||
|
||||
def levels(self) -> list[list[Package]]:
|
||||
"""
|
||||
get build levels starting from the packages which do not require any other package to build
|
||||
@ -155,8 +241,10 @@ class Tree:
|
||||
# build initial tree
|
||||
unprocessed = self.leaves[:]
|
||||
while unprocessed:
|
||||
unsorted.append([leaf for leaf in unprocessed if leaf.is_root(unprocessed)])
|
||||
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
|
||||
# additional workaround with partial in order to hide cell-var-from-loop pylint warning
|
||||
predicate = functools.partial(Leaf.is_root, packages=unprocessed)
|
||||
new_level, unprocessed = partition(unprocessed, predicate)
|
||||
unsorted.append(new_level)
|
||||
|
||||
# move leaves to the end if they are not required at the next level
|
||||
for current_num, current_level in enumerate(unsorted[:-1]):
|
||||
@ -164,13 +252,47 @@ class Tree:
|
||||
next_level = unsorted[next_num]
|
||||
|
||||
# change lists inside the collection
|
||||
# additional workaround with partial in order to hide cell-var-from-loop pylint warning
|
||||
predicate = functools.partial(Leaf.is_dependency, packages=next_level)
|
||||
unsorted[current_num], to_be_moved = partition(current_level, predicate)
|
||||
unsorted[next_num].extend(to_be_moved)
|
||||
|
||||
comparator: Callable[[Package], str] = lambda package: package.base
|
||||
return [
|
||||
sorted([leaf.package for leaf in level], key=comparator)
|
||||
for level in unsorted if level
|
||||
]
|
||||
return self.sort(unsorted)
|
||||
|
||||
def partitions(self, *, count: int) -> list[list[Package]]:
|
||||
"""
|
||||
partition tree into (more or less) equal chunks of packages which don't depend on each other
|
||||
|
||||
Args:
|
||||
count(int): maximal amount of partitions
|
||||
|
||||
Returns:
|
||||
list[list[Package]]: sorted list of packages partitions
|
||||
"""
|
||||
unsorted: list[list[Leaf]] = [[] for _ in range(count)]
|
||||
|
||||
# in order to keep result stable we will need to sort packages all times
|
||||
unprocessed = sorted(self.leaves, key=lambda leaf: leaf.package.base)
|
||||
while unprocessed:
|
||||
# pick one and append it to the most free partition and build chunk
|
||||
leaf = unprocessed.pop()
|
||||
chunk = [leaf]
|
||||
|
||||
while True: # python doesn't allow to use walrus operator to unpack tuples
|
||||
# get packages which depend on packages in chunk
|
||||
predicate = functools.partial(Leaf.is_root, packages=chunk)
|
||||
unprocessed, new_dependent = partition(unprocessed, predicate)
|
||||
chunk.extend(new_dependent)
|
||||
|
||||
# get packages which are dependency of packages in chunk
|
||||
predicate = functools.partial(Leaf.is_dependency, packages=chunk)
|
||||
new_dependencies, unprocessed = partition(unprocessed, predicate)
|
||||
chunk.extend(new_dependencies)
|
||||
|
||||
if not new_dependent and not new_dependencies:
|
||||
break
|
||||
|
||||
part = min(unsorted, key=len)
|
||||
part.extend(chunk)
|
||||
|
||||
balanced = self.balance(unsorted)
|
||||
return self.sort(balanced)
|
||||
|
@ -46,6 +46,7 @@ __all__ = [
|
||||
"extract_user",
|
||||
"filter_json",
|
||||
"full_version",
|
||||
"minmax",
|
||||
"package_like",
|
||||
"parse_version",
|
||||
"partition",
|
||||
@ -263,6 +264,22 @@ def full_version(epoch: str | int | None, pkgver: str, pkgrel: str) -> str:
|
||||
return f"{prefix}{pkgver}-{pkgrel}"
|
||||
|
||||
|
||||
def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tuple[T, T]:
|
||||
"""
|
||||
get min and max value from iterable
|
||||
|
||||
Args:
|
||||
source(Iterable[T]): source list to find min and max values
|
||||
key(Callable[[T], Any] | None, optional): key to sort (Default value = None)
|
||||
|
||||
Returns:
|
||||
tuple[T, T]: min and max values for sequence
|
||||
"""
|
||||
first_iter, second_iter = itertools.tee(source)
|
||||
# typing doesn't expose SupportLessThan, so we just ignore this in typecheck
|
||||
return min(first_iter, key=key), max(second_iter, key=key) # type: ignore
|
||||
|
||||
|
||||
def package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
@ -296,12 +313,12 @@ def parse_version(version: str) -> tuple[str | None, str, str]:
|
||||
return epoch, pkgver, pkgrel
|
||||
|
||||
|
||||
def partition(source: list[T], predicate: Callable[[T], bool]) -> tuple[list[T], list[T]]:
|
||||
def partition(source: Iterable[T], predicate: Callable[[T], bool]) -> tuple[list[T], list[T]]:
|
||||
"""
|
||||
partition list into two based on predicate, based on https://docs.python.org/dev/library/itertools.html#itertools-recipes
|
||||
|
||||
Args:
|
||||
source(list[T]): source list to be partitioned
|
||||
source(Iterable[T]): source list to be partitioned
|
||||
predicate(Callable[[T], bool]): filter function
|
||||
|
||||
Returns:
|
||||
|
@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.application.handlers import Structure
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -8,19 +9,40 @@ from ahriman.core.repository import Repository
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.partitions = 1
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
|
||||
packages_mock = mocker.patch("ahriman.core.tree.Tree.partition", return_value=[[package_ahriman]])
|
||||
application_mock = mocker.patch("ahriman.core.tree.Tree.resolve", return_value=[[package_ahriman]])
|
||||
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||
|
||||
Structure.run(args, "x86_64", configuration, report=False)
|
||||
packages_mock.assert_called_once_with([package_ahriman], count=args.partitions)
|
||||
application_mock.assert_called_once_with([package_ahriman])
|
||||
print_mock.assert_called_once_with(verbose=True, separator=" ")
|
||||
print_mock.assert_has_calls([
|
||||
MockCall(verbose=False),
|
||||
MockCall(verbose=True, separator=" "),
|
||||
MockCall(verbose=False),
|
||||
])
|
||||
|
||||
|
||||
def test_disallow_auto_architecture_run() -> None:
|
||||
|
@ -600,6 +600,16 @@ def test_subparsers_repo_tree_architecture(parser: argparse.ArgumentParser) -> N
|
||||
assert args.architecture == ["x86_64"]
|
||||
|
||||
|
||||
def test_subparsers_repo_tree_option_partitions(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
must convert partitions option to int instance
|
||||
"""
|
||||
args = parser.parse_args(["repo-tree"])
|
||||
assert isinstance(args.partitions, int)
|
||||
args = parser.parse_args(["repo-tree", "--partitions", "42"])
|
||||
assert isinstance(args.partitions, int)
|
||||
|
||||
|
||||
def test_subparsers_repo_triggers_architecture(parser: argparse.ArgumentParser) -> None:
|
||||
"""
|
||||
repo-triggers command must correctly parse architecture list
|
||||
|
@ -1,6 +1,11 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.exceptions import PartitionError
|
||||
from ahriman.core.tree import Leaf, Tree
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
|
||||
|
||||
def test_leaf_is_root_empty(leaf_ahriman: Leaf) -> None:
|
||||
@ -33,15 +38,100 @@ def test_leaf_is_root_true(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> No
|
||||
assert not leaf_ahriman.is_root([leaf_python_schedule])
|
||||
|
||||
|
||||
def test_tree_balance() -> None:
|
||||
"""
|
||||
must balance partitions
|
||||
"""
|
||||
leaf1 = Leaf(
|
||||
Package(
|
||||
base="package1",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package1": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf2 = Leaf(
|
||||
Package(
|
||||
base="package2",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package2": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf3 = Leaf(
|
||||
Package(
|
||||
base="package3",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package3": PackageDescription(depends=["package1"])},
|
||||
)
|
||||
)
|
||||
leaf4 = Leaf(
|
||||
Package(
|
||||
base="package4",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package4": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf5 = Leaf(
|
||||
Package(
|
||||
base="package5",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package5": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
first, second, third = Tree.balance([[leaf4], [leaf1, leaf2, leaf3], [leaf5]])
|
||||
assert first == [leaf4, leaf2]
|
||||
assert second == [leaf1, leaf3]
|
||||
assert third == [leaf5]
|
||||
|
||||
|
||||
def test_tree_partition(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must partition dependencies tree
|
||||
"""
|
||||
partitions = Tree.partition([package_ahriman, package_python_schedule], count=1)
|
||||
assert len(partitions) == 1
|
||||
assert len(partitions[0]) == 2
|
||||
|
||||
partitions = Tree.partition([package_ahriman, package_python_schedule], count=2)
|
||||
assert len(partitions) == 2
|
||||
assert all(len(partition) < 2 for partition in partitions)
|
||||
|
||||
partitions = Tree.partition([package_ahriman, package_python_schedule], count=3)
|
||||
assert len(partitions) == 2
|
||||
assert all(len(partition) < 2 for partition in partitions)
|
||||
|
||||
|
||||
def test_tree_partition_invalid_count() -> None:
|
||||
"""
|
||||
must raise PartitionError exception if count is invalid
|
||||
"""
|
||||
with pytest.raises(PartitionError):
|
||||
Tree.partition([], count=0)
|
||||
|
||||
with pytest.raises(PartitionError):
|
||||
Tree.partition([], count=-1)
|
||||
|
||||
|
||||
def test_tree_resolve(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must resolve denendecnies tree
|
||||
must resolve dependencies tree
|
||||
"""
|
||||
tree = Tree.resolve([package_ahriman, package_python_schedule])
|
||||
assert len(tree) == 1
|
||||
assert len(tree[0]) == 2
|
||||
|
||||
|
||||
def test_tree_sort(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
|
||||
"""
|
||||
must sort leaves and return packages
|
||||
"""
|
||||
assert Tree.sort([[leaf_python_schedule, leaf_ahriman]]) == [[leaf_ahriman.package, leaf_python_schedule.package]]
|
||||
|
||||
|
||||
def test_tree_levels(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
|
||||
"""
|
||||
must generate correct levels in the simples case
|
||||
@ -62,32 +152,32 @@ def test_tree_levels_sorted() -> None:
|
||||
Package(
|
||||
base="package1",
|
||||
version="1.0.0",
|
||||
remote=None,
|
||||
packages={"package1": PackageDescription(depends=[])}
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package1": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf2 = Leaf(
|
||||
Package(
|
||||
base="package2",
|
||||
version="1.0.0",
|
||||
remote=None,
|
||||
packages={"package2": PackageDescription(depends=["package1"])}
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package2": PackageDescription(depends=["package1"])},
|
||||
)
|
||||
)
|
||||
leaf3 = Leaf(
|
||||
Package(
|
||||
base="package3",
|
||||
version="1.0.0",
|
||||
remote=None,
|
||||
packages={"package3": PackageDescription(depends=["package1"])}
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package3": PackageDescription(depends=["package1"])},
|
||||
)
|
||||
)
|
||||
leaf4 = Leaf(
|
||||
Package(
|
||||
base="package4",
|
||||
version="1.0.0",
|
||||
remote=None,
|
||||
packages={"package4": PackageDescription(depends=["package3"])}
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package4": PackageDescription(depends=["package3"])},
|
||||
)
|
||||
)
|
||||
|
||||
@ -96,3 +186,54 @@ def test_tree_levels_sorted() -> None:
|
||||
assert first == [leaf1.package]
|
||||
assert second == [leaf3.package]
|
||||
assert third == [leaf2.package, leaf4.package]
|
||||
|
||||
|
||||
def test_tree_partitions() -> None:
|
||||
"""
|
||||
must divide tree into partitions
|
||||
"""
|
||||
leaf1 = Leaf(
|
||||
Package(
|
||||
base="package1",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package1": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf2 = Leaf(
|
||||
Package(
|
||||
base="package2",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package2": PackageDescription(depends=["package1"])},
|
||||
)
|
||||
)
|
||||
leaf3 = Leaf(
|
||||
Package(
|
||||
base="package3",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package3": PackageDescription(depends=["package1"])},
|
||||
)
|
||||
)
|
||||
leaf4 = Leaf(
|
||||
Package(
|
||||
base="package4",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package4": PackageDescription(depends=[])},
|
||||
)
|
||||
)
|
||||
leaf5 = Leaf(
|
||||
Package(
|
||||
base="package5",
|
||||
version="1.0.0",
|
||||
remote=RemoteSource(source=PackageSource.AUR),
|
||||
packages={"package5": PackageDescription(depends=["package2"])},
|
||||
)
|
||||
)
|
||||
|
||||
tree = Tree([leaf1, leaf2, leaf3, leaf4, leaf5])
|
||||
first, second = tree.partitions(count=3)
|
||||
assert first == [leaf1.package, leaf2.package, leaf3.package, leaf5.package]
|
||||
assert second == [leaf4.package]
|
||||
|
@ -2,16 +2,15 @@ import datetime
|
||||
import logging
|
||||
import os
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError
|
||||
from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \
|
||||
full_version, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
||||
full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \
|
||||
srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
@ -204,6 +203,15 @@ def test_dataclass_view_without_none(package_ahriman: Package) -> None:
|
||||
assert Package.from_json(result) == package_ahriman
|
||||
|
||||
|
||||
def test_enum_values() -> None:
|
||||
"""
|
||||
must correctly generate choices from enumeration classes
|
||||
"""
|
||||
values = enum_values(PackageSource)
|
||||
for value in values:
|
||||
assert PackageSource(value).value == value
|
||||
|
||||
|
||||
def test_extract_user() -> None:
|
||||
"""
|
||||
must extract user from system environment
|
||||
@ -241,15 +249,6 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None:
|
||||
assert "base" not in filter_json(probe, probe.keys())
|
||||
|
||||
|
||||
def test_enum_values() -> None:
|
||||
"""
|
||||
must correctly generate choices from enumeration classes
|
||||
"""
|
||||
values = enum_values(PackageSource)
|
||||
for value in values:
|
||||
assert PackageSource(value).value == value
|
||||
|
||||
|
||||
def test_full_version() -> None:
|
||||
"""
|
||||
must construct full version
|
||||
@ -260,6 +259,14 @@ def test_full_version() -> None:
|
||||
assert full_version(1, "0.12.1", "1") == "1:0.12.1-1"
|
||||
|
||||
|
||||
def test_minmax() -> None:
|
||||
"""
|
||||
must correctly define minimal and maximal value
|
||||
"""
|
||||
assert minmax([1, 4, 3, 2]) == (1, 4)
|
||||
assert minmax([[1, 2, 3], [4, 5], [6, 7, 8, 9]], key=len) == ([4, 5], [6, 7, 8, 9])
|
||||
|
||||
|
||||
def test_package_like(package_ahriman: Package) -> None:
|
||||
"""
|
||||
package_like must return true for archives
|
||||
|
Loading…
Reference in New Issue
Block a user