diff --git a/CHANGELOG.md b/CHANGELOG.md index aad7f6e1..6004753e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - Arch - - not properly mapping all provided packages by the system when checking for upgrade requirements + - not properly checking all provided packages by the system when checking for conflicts (upgrade requirements) + +### Improvements +- Arch + - added several test cases to ensure conflicting scenarios are properly covered when checking for upgrade requirements ## [0.10.4] 2022-11-05 diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index dd3e3e5e..9f9b716c 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -58,7 +58,7 @@ class SearchResult: class UpgradeRequirement: - def __init__(self, pkg: SoftwarePackage, reason: str = None, required_size: float = None, extra_size: float = None, sorting_priority: int = 0): + def __init__(self, pkg: SoftwarePackage, reason: Optional[str] = None, required_size: Optional[float] = None, extra_size: Optional[float] = None, sorting_priority: int = 0): """ :param pkg: @@ -77,6 +77,15 @@ class UpgradeRequirement: def sort_by_priority(req: "UpgradeRequirement") -> Tuple[int, str]: return -req.sorting_priority, req.pkg.name + def __eq__(self, other) -> bool: + if isinstance(other, UpgradeRequirement): + return self.__dict__ == other.__dict__ + + return False + + def __hash__(self) -> int: + return sum((hash(k) + hash(v) for k, v in self.__dict__.items())) + class UpgradeRequirements: diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index f84ff783..0db7484c 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -464,9 +464,9 @@ class DependenciesAnalyser: if missing_deps: self._fill_single_providers_data(missing_deps, repo_missing, aur_missing, deps_data) - missing_subdeps = self.map_missing_deps(pkgs_data={**deps_data}, provided_map=provided_map, aur_index=aur_index, - deps_checked=deps_checked, sort=False, deps_data=deps_data, - watcher=watcher, + missing_subdeps = self.map_missing_deps(pkgs_data={**deps_data}, provided_map=provided_map, + aur_index=aur_index, deps_checked=deps_checked, sort=False, + deps_data=deps_data, watcher=watcher, remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map, automatch_providers=automatch_providers, @@ -650,7 +650,7 @@ class DependenciesAnalyser: if pkgnames: to_ignore.update(pkgnames) - all_requirements = {req for reqs in pacman.map_required_by(pkgnames).values() for req in reqs if req not in to_ignore} + all_requirements = {r for reqs in pacman.map_required_by(pkgnames).values() for r in reqs if r not in to_ignore} if all_requirements: sub_requirements = self.map_all_required_by(all_requirements, to_ignore) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 9cc15a8e..f5be87d3 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -396,7 +396,7 @@ def get_version_for_not_installed(pkgname: str) -> str: def map_repositories(pkgnames: Iterable[str] = None) -> Dict[str, str]: - info = run_cmd('pacman -Si {}'.format(' '.join(pkgnames) if pkgnames else ''), print_error=False, ignore_return_code=True) + info = run_cmd(f"pacman -Si {' '.join(pkgnames) if pkgnames else ''}", print_error=False, ignore_return_code=True) if info: repos = re.findall(r'(Name|Repository)\s*:\s*(.+)', info) @@ -529,7 +529,7 @@ def upgrade_system(root_password: Optional[str]) -> SimpleProcess: return SimpleProcess(cmd=['pacman', '-Syyu', '--noconfirm'], root_password=root_password) -def fill_provided_map(key: str, val: str, output: dict): +def _fill_provided_map(key: str, val: str, output: Dict[str, Set[str]]): current_val = output.get(key) if current_val is None: @@ -558,19 +558,19 @@ def map_provided(remote: bool = False, pkgs: Iterable[str] = None) -> Optional[D elif field == 'Version': latest_version = val.split('=')[0] elif field == 'Provides': - fill_provided_map(latest_name, latest_name, provided_map) - fill_provided_map(f'{latest_name}={latest_version}', latest_name, provided_map) + _fill_provided_map(latest_name, latest_name, provided_map) + _fill_provided_map(f'{latest_name}={latest_version}', latest_name, provided_map) if val != 'None': for w in val.split(' '): if w: word = w.strip() - fill_provided_map(word, latest_name, provided_map) + _fill_provided_map(word, latest_name, provided_map) word_split = word.split('=') if word_split[0] != word: - fill_provided_map(word_split[0], latest_name, provided_map) + _fill_provided_map(word_split[0], latest_name, provided_map) else: provided = True @@ -583,12 +583,12 @@ def map_provided(remote: bool = False, pkgs: Iterable[str] = None) -> Optional[D for w in l.split(' '): if w: word = w.strip() - fill_provided_map(word, latest_name, provided_map) + _fill_provided_map(word, latest_name, provided_map) word_split = word.split('=') if word_split[0] != word: - fill_provided_map(word_split[0], latest_name, provided_map) + _fill_provided_map(word_split[0], latest_name, provided_map) return provided_map @@ -857,7 +857,8 @@ def get_cache_dir() -> str: def map_required_by(names: Iterable[str] = None, remote: bool = False) -> Dict[str, Set[str]]: - output = run_cmd('pacman -{} {}'.format('Sii' if remote else 'Qi', ' '.join(names) if names else ''), print_error=False) + output = run_cmd(f"pacman -{'Sii' if remote else 'Qi'} {' '.join(names) if names else ''}".strip(), + print_error=False) if output: latest_name, required = None, None diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 080b414b..1376ca2f 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -2,7 +2,7 @@ import logging import time import traceback from threading import Thread -from typing import Dict, Set, List, Tuple, Iterable, Optional +from typing import Dict, Set, List, Tuple, Iterable, Optional, Any from bauh.api.abstract.controller import UpgradeRequirements, UpgradeRequirement from bauh.api.abstract.handler import ProcessWatcher @@ -47,12 +47,26 @@ class UpdateRequirementsContext: if self.provided_map is None: self.provided_map = {**update} else: - for key, val in update.items(): - if key not in self.provided_map: - self.provided_map[key] = val - elif val: - current_val = self.provided_map[key] - current_val.update(val) + for provider, provided in update.items(): + provided_set = self.provided_map.get(provider) + + if provided_set is None: + provided_set = set() + self.provided_map[provider] = provided_set + + provided_set.update(provided) + + def add_to_provided_map(self, provider: str, provided: str): + if self.provided_map is None: + self.provided_map = dict() + + provided_set = self.provided_map.get(provider) + + if provided_set is None: + provided_set = set() + self.provided_map[provider] = provided_set + + provided_set.add(provided) class UpdatesSummarizer: @@ -72,8 +86,7 @@ class UpdatesSummarizer: for src_pkg in {p for p, data in context.pkgs_data.items() if data['d'] and pkg1 in data['d'] or pkg2 in data['d']}: if src_pkg not in context.cannot_upgrade: - reason = self.i18n['arch.update_summary.to_install.dep_conflict'].format("'{}'".format(pkg1), - "'{}'".format(pkg2)) + reason = self.i18n['arch.update_summary.to_install.dep_conflict'].format(f"'{pkg1}'", f"'{pkg2}'") context.cannot_upgrade[src_pkg] = UpgradeRequirement(context.to_update[src_pkg], reason) del context.to_update[src_pkg] @@ -137,11 +150,11 @@ class UpdatesSummarizer: def _handle_conflict_both_to_update(self, pkg1: str, pkg2: str, context: UpdateRequirementsContext): if pkg1 not in context.cannot_upgrade: - reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), pkg2) + reason = f"{self.i18n['arch.info.conflicts with'].capitalize()} '{pkg2}'" context.cannot_upgrade[pkg1] = UpgradeRequirement(pkg=context.to_update[pkg1], reason=reason) if pkg2 not in context.cannot_upgrade: - reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), pkg1) + reason = f"{self.i18n['arch.info.conflicts with'].capitalize()} '{pkg1}'" context.cannot_upgrade[pkg2] = UpgradeRequirement(pkg=context.to_update[pkg2], reason=reason) for p in (pkg1, pkg2): @@ -153,65 +166,101 @@ class UpdatesSummarizer: else: del context.aur_to_update[p] - def _filter_and_map_conflicts(self, context: UpdateRequirementsContext) -> Dict[str, str]: + def _map_conflicts(self, data: Dict[str, Dict[str, Any]], providers: Dict[str, Set[str]], + versions: Dict[str, str]) -> Tuple[Dict[str, str], Dict[str, str]]: + """ + Parameters + pkgs_data: a dict mapping the packages whose conflicts need to be analyzed to their data + providers: a dict mapping the available providers on the context (installed and to be installed) to their + respective package + versions: a dict mapping the package name to it's version (the updated or to be installed version) + Return + a tuple with two dictionaries: + - first: containing all conflicts + - second: containing mutual conflicts + """ root_conflict = {} mutual_conflicts = {} - for p, data in context.pkgs_data.items(): + for pkg_name, data in data.items(): if data['c']: for c in data['c']: if c: name_op_exp = DependenciesAnalyser.re_dep_operator().split(c) conflict_name = name_op_exp[0] - if conflict_name != p: - conflict_version = context.installed.get(conflict_name) + if conflict_name != pkg_name: + conflict_providers = providers.get(conflict_name) - if conflict_version: - if len(name_op_exp) == 1 or match_required_version(conflict_version, - name_op_exp[1], - name_op_exp[2]): - root_conflict[conflict_name] = p + if conflict_providers: + checked_conflicts = [] + for provider in conflict_providers: + if provider != pkg_name: + if len(name_op_exp) == 1: + checked_conflicts.append(provider) + else: + provider_version = versions.get(provider) - if (p, conflict_name) in root_conflict.items(): - mutual_conflicts[conflict_name] = p + if match_required_version(provider_version, + name_op_exp[1], + name_op_exp[2]): + checked_conflicts.append(provider) - if mutual_conflicts: - for pkg1, pkg2 in mutual_conflicts.items(): - pkg1_to_install = pkg1 in context.to_install - pkg2_to_install = pkg2 in context.to_install + for provider in checked_conflicts: + root_conflict[provider] = pkg_name - if pkg1_to_install and pkg2_to_install: # remove both from to install and mark their source packages as 'cannot_update' - self._handle_conflict_both_to_install(pkg1, pkg2, context) - elif (pkg1_to_install and not pkg2_to_install) or (not pkg1_to_install and pkg2_to_install): - self._handle_conflict_to_update_and_to_install(pkg1, pkg2, pkg1_to_install, context) - else: - self._handle_conflict_both_to_update(pkg1, pkg2, context) # adding both to the 'cannot update' list + if (pkg_name, provider) in root_conflict.items(): + mutual_conflicts[provider] = pkg_name - for pkg1, pkg2 in mutual_conflicts.items(): # removing conflicting packages from the packages selected to upgrade - for p in (pkg1, pkg2): - if p in context.pkgs_data: - if context.pkgs_data[p].get('c'): - for c in context.pkgs_data[p]['c']: - # source = provided_map[c] - if c in root_conflict: - del root_conflict[c] + return root_conflict, mutual_conflicts - del context.pkgs_data[p] + def _handle_mutual_conflicts(self, mutual_conflicts: Dict[str, str], all_conflicts: Dict[str, str], + context: UpdateRequirementsContext): + for pkg1, pkg2 in mutual_conflicts.items(): + pkg1_to_install = pkg1 in context.to_install + pkg2_to_install = pkg2 in context.to_install - return root_conflict + if pkg1_to_install and pkg2_to_install: + # remove both from to install and mark their source packages as 'cannot_update' + self._handle_conflict_both_to_install(pkg1, pkg2, context) + elif (pkg1_to_install and not pkg2_to_install) or (not pkg1_to_install and pkg2_to_install): + self._handle_conflict_to_update_and_to_install(pkg1, pkg2, pkg1_to_install, context) + else: + # adding both to the 'cannot update' list + self._handle_conflict_both_to_update(pkg1, pkg2, context) - def _fill_conflicts(self, context: UpdateRequirementsContext, blacklist: Iterable[str] = None): + # removing conflicting packages from the packages selected to upgrade + for pkg1, pkg2 in mutual_conflicts.items(): + for pkg_name in (pkg1, pkg2): + if pkg_name in context.pkgs_data: + if context.pkgs_data[pkg_name].get('c'): + for c in context.pkgs_data[pkg_name]['c']: + # source = provided_map[c] + if c in all_conflicts: + del all_conflicts[c] + + del context.pkgs_data[pkg_name] + + def _fill_conflicts(self, context: UpdateRequirementsContext, blacklist: Optional[Iterable[str]] = None): self.logger.info("Checking conflicts") - root_conflict = self._filter_and_map_conflicts(context) + conflicts, mutual_conflicts = self._map_conflicts(data=context.pkgs_data, + providers=context.provided_map, + versions=context.installed) - if root_conflict: - for dep, source in root_conflict.items(): - if dep not in context.to_remove and (not blacklist or dep not in blacklist): - req = ArchPackage(name=dep, installed=True, i18n=self.i18n) - reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), source) - context.to_remove[dep] = UpgradeRequirement(req, reason) + if mutual_conflicts: + self._handle_mutual_conflicts(mutual_conflicts, conflicts, context) + + if conflicts: + for conflict_name, source_name in conflicts.items(): + if conflict_name not in context.to_remove and (not blacklist or conflict_name not in blacklist): + if conflict_name in context.to_update: + conflict = context.to_update[conflict_name] + else: + conflict = ArchPackage(name=conflict_name, installed=True, i18n=self.i18n) + + reason = f"{self.i18n['arch.info.conflicts with'].capitalize()} '{source_name}'" + context.to_remove[conflict_name] = UpgradeRequirement(conflict, reason) def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict): version = None @@ -234,6 +283,31 @@ class UpdatesSummarizer: output[idx] = ArchPackage(name=pkg_data[0], version=version, latest_version=version, repository=pkg_data[1], i18n=self.i18n) + def _fill_conflicts_to_install(self, context: UpdateRequirementsContext, install_data: Dict[str, Dict[str, Any]]): + """ + Parameters + context: update context + install_data: a dict mapping the packages to be installed names by their data + """ + # to properly fill conflicts considering new packages to be installed: + # - the 'context.provided_map' should contain the providers of these new packages + # - the 'context.installed' should contain the versions of these new packages + provided_map_bkp = {**context.provided_map} + self.__fill_provided_map(context=context, pkgs=context.to_install, fill_installed=False) + + # adding the new packages to install as 'installed' + for pkg, data in install_data.items(): + context.installed[pkg] = data["v"] + + self._fill_conflicts(context, context.to_remove.keys()) + + # restoring the original data structures + context.provided_map = provided_map_bkp + + for pkg in install_data: + if pkg in context.installed: + del context.installed[pkg] + def _fill_to_install(self, context: UpdateRequirementsContext) -> bool: ti = time.time() self.logger.info("Discovering updates missing packages") @@ -266,7 +340,8 @@ class UpdatesSummarizer: for idx, dep in enumerate(deps): data = deps_data[dep[0]] - pkg = ArchPackage(name=dep[0], version=data['v'], latest_version=data['v'], repository=dep[1], i18n=self.i18n, package_base=data.get('b', dep[0])) + pkg = ArchPackage(name=dep[0], version=data['v'], latest_version=data['v'], repository=dep[1], + i18n=self.i18n, package_base=data.get('b', dep[0])) sorted_pkgs[idx] = pkg context.to_install[dep[0]] = pkg @@ -284,16 +359,17 @@ class UpdatesSummarizer: if all_to_install_data: context.pkgs_data.update(all_to_install_data) - self._fill_conflicts(context, context.to_remove.keys()) + self._fill_conflicts_to_install(context, all_to_install_data) if context.to_install: self.__fill_provided_map(context=context, pkgs=context.to_install, fill_installed=False) tf = time.time() - self.logger.info("It took {0:.2f} seconds to retrieve required upgrade packages".format(tf - ti)) + self.logger.info(f"It took {tf - ti:.2f} seconds to retrieve required upgrade packages") return True - def __fill_provided_map(self, context: UpdateRequirementsContext, pkgs: Dict[str, ArchPackage], fill_installed: bool = True): + def __fill_provided_map(self, context: UpdateRequirementsContext, pkgs: Dict[str, ArchPackage], + fill_installed: bool = True): if pkgs: ti = time.time() self.logger.info("Filling provided names") @@ -304,26 +380,26 @@ class UpdatesSummarizer: installed_to_ignore = set() for pkgname in pkgs: - pacman.fill_provided_map(pkgname, pkgname, context.provided_map) + context.add_to_provided_map(pkgname, pkgname) if fill_installed: installed_to_ignore.add(pkgname) pdata = context.pkgs_data.get(pkgname) if pdata and pdata['p']: - pacman.fill_provided_map('{}={}'.format(pkgname, pdata['v']), pkgname, context.provided_map) + context.add_to_provided_map(f"{pkgname}={pdata['v']}", pkgname) ver_split = pdata['v'].split('-') if len(ver_split) > 1: - pacman.fill_provided_map('{}={}'.format(pkgname, '-'.join(ver_split[0:-1])), pkgname, context.provided_map) + context.add_to_provided_map(f"{pkgname}={'-'.join(ver_split[0:-1])}", pkgname) for p in pdata['p']: - pacman.fill_provided_map(p, pkgname, context.provided_map) + context.add_to_provided_map(p, pkgname) split_provided = p.split('=') if len(split_provided) > 1 and split_provided[0] != p: - pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map) + context.add_to_provided_map(split_provided[0], pkgname) if context.installed and installed_to_ignore: # filling the provided names of the installed installed_to_query = {*context.installed}.difference(installed_to_ignore) @@ -343,7 +419,10 @@ class UpdatesSummarizer: context.aur_index.update(names) self.logger.info("AUR index loaded on the context") - def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_sync: Set[str] = None) -> UpgradeRequirement: + def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, + installed_sizes: Optional[Dict[str, float]] = None, to_install: bool = False, + to_sync: Set[str] = None) -> UpgradeRequirement: + requirement = UpgradeRequirement(pkg) if pkg.repository != 'aur': @@ -383,13 +462,15 @@ class UpdatesSummarizer: required_by.add(p) break - requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), ','.join(required_by) if required_by else '?') + requirement.reason = f"{self.i18n['arch.info.required by'].capitalize()}: " \ + f"{','.join(required_by) if required_by else '?'}" return requirement - def summarize(self, pkgs: List[ArchPackage], root_password: Optional[str], arch_config: dict) -> Optional[UpgradeRequirements]: - res = UpgradeRequirements([], [], [], []) + def summarize(self, pkgs: List[ArchPackage], root_password: Optional[str], arch_config: dict) \ + -> Optional[UpgradeRequirements]: + res = UpgradeRequirements([], [], [], []) remote_provided_map = pacman.map_provided(remote=True) remote_repo_map = pacman.map_repositories() context = UpdateRequirementsContext(to_update={}, repo_to_update={}, aur_to_update={}, repo_to_install={}, @@ -434,20 +515,22 @@ class UpdatesSummarizer: self.logger.info("The operation was cancelled by the user") return except PackageNotFoundException as e: - self.logger.error("Package '{}' not found".format(e.name)) + self.logger.error(f"Package '{e.name}' not found") return if context.pkgs_data: self._fill_dependency_breakage(context) - self.__update_context_based_on_to_remove(context) + if context.to_remove: + self.__update_context_based_on_to_remove(context) if context.to_update: installed_sizes = pacman.get_installed_size(list(context.to_update.keys())) sorted_pkgs = [] - if context.repo_to_update: # only sorting by name ( pacman already knows the best order to perform the upgrade ) + if context.repo_to_update: + # only sorting by name (pacman already knows the best order to perform the upgrade) sorted_pkgs.extend(context.repo_to_update.values()) sorted_pkgs.sort(key=lambda pkg: pkg.name) @@ -468,101 +551,121 @@ class UpdatesSummarizer: if context.to_install: to_sync = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else {} to_sync.update(context.to_install.keys()) - res.to_install = [self._map_requirement(p, context, to_install=True, to_sync=to_sync) for p in context.to_install.values()] + res.to_install = [self._map_requirement(p, context, to_install=True, to_sync=to_sync) + for p in context.to_install.values()] res.context['data'] = context.pkgs_data return res def __update_context_based_on_to_remove(self, context: UpdateRequirementsContext): - if context.to_remove: - to_remove_provided = {} - # filtering all package to synchronization from the transaction context - to_sync = {*(context.to_update.keys() if context.to_update else set()), *(context.to_install.keys() if context.to_install else set())} - if to_sync: # checking if any packages to sync on the context rely on the 'to remove' ones - to_remove_provided.update(pacman.map_provided(remote=False, pkgs=context.to_remove.keys())) - to_remove_from_sync = {} # will store all packages that should be removed + # filtering all package to synchronization from the transaction context + to_sync = set() - for pname in to_sync: - if pname in context.pkgs_data: - deps = context.pkgs_data[pname].get('d') + if context.to_update: + to_sync.update(context.to_update.keys()) - if deps: - required = set() + if context.to_install: + to_sync.update(context.to_install.keys()) - for pkg in context.to_remove: - for provided in to_remove_provided[pkg]: - if provided in deps: - required.add(pkg) - break + to_remove_provided = {} + if to_sync: # checking if any packages to sync on the context rely on the 'to remove' ones + to_remove_provided.update(pacman.map_provided(remote=False, pkgs=context.to_remove.keys())) + to_remove_from_sync = {} # will store all packages that should be removed - if required: - to_remove_from_sync[pname] = required + for pname in to_sync: + if pname in context.pkgs_data: + deps = context.pkgs_data[pname].get('d') + + if deps: + required = set() + + for pkg in context.to_remove: + for provided in to_remove_provided[pkg]: + if provided in deps: + required.add(pkg) + break + + if required: + to_remove_from_sync[pname] = required + else: + self.logger.warning(f"Conflict resolution: package '{pname}' marked to synchronization " + f"has no data loaded") + + if to_remove_from_sync: # removing all these packages and their dependents from the context + self._add_to_remove(to_sync, to_remove_from_sync, context) + + # checking if the installed packages that are not in the transaction context rely on the current + # packages to be removed: + current_to_remove = {*context.to_remove.keys()} + required_by_to_remove = self.deps_analyser.map_all_required_by(current_to_remove, {*to_sync}) + + if required_by_to_remove: + # updating provided context: + provided_not_mapped = set() + for pkg in current_to_remove.difference({*to_remove_provided.keys()}): + if pkg not in context.pkgs_data: + provided_not_mapped.add(pkg) + else: + provided = context.pkgs_data[pkg].get('p') + if provided: + to_remove_provided[pkg] = provided else: - self.logger.warning("Conflict resolution: package '{}' marked to synchronization has no data loaded") - - if to_remove_from_sync: # removing all these packages and their dependents from the context - self._add_to_remove(to_sync, to_remove_from_sync, context) - - # checking if the installed packages that are not in the transaction context rely on the current packages to be removed: - current_to_remove = {*context.to_remove.keys()} - required_by_installed = self.deps_analyser.map_all_required_by(current_to_remove, {*to_sync}) - - if required_by_installed: - # updating provided context: - provided_not_mapped = set() - for pkg in current_to_remove.difference({*to_remove_provided.keys()}): - if pkg not in context.pkgs_data: provided_not_mapped.add(pkg) + + if provided_not_mapped: + to_remove_provided.update(pacman.map_provided(remote=False, pkgs=provided_not_mapped)) + + deps_no_data = {dep for dep in required_by_to_remove if dep not in context.pkgs_data} + deps_nodata_deps = pacman.map_required_dependencies(*deps_no_data) if deps_no_data else {} + + reverse_to_remove_provided = {p: name for name, provided in to_remove_provided.items() for p in provided} + + for pkg in required_by_to_remove: + if pkg not in context.to_remove: + if pkg in context.pkgs_data: + dep_deps = context.pkgs_data[pkg].get('d') else: - provided = context.pkgs_data[pkg].get('p') - if provided: - to_remove_provided[pkg] = provided - else: - provided_not_mapped.add(pkg) + dep_deps = deps_nodata_deps.get(pkg) - if provided_not_mapped: - to_remove_provided.update(pacman.map_provided(remote=False, pkgs=provided_not_mapped)) + if dep_deps: + source = ', '.join( + (reverse_to_remove_provided[d] for d in dep_deps if d in reverse_to_remove_provided)) + reason = f"{self.i18n['arch.info.depends on'].capitalize()} '{source if source else '?'}'" + else: + reason = '?' - deps_no_data = {dep for dep in required_by_installed if dep in context.pkgs_data} - deps_nodata_deps = pacman.map_required_dependencies(*deps_no_data) if deps_no_data else {} + pkg_repo = context.remote_repo_map.get(pkg) + pkg_version = context.installed.get(pkg) + context.to_remove[pkg] = UpgradeRequirement(pkg=ArchPackage(name=pkg, + installed=True, + i18n=self.i18n, + version=pkg_version, + latest_version=pkg_version, + repository=pkg_repo), + reason=reason) - reverse_to_remove_provided = {p: name for name, provided in to_remove_provided.items() for p in provided} + for name in context.to_remove: # upgrading lists + if name in context.pkgs_data: + del context.pkgs_data[name] - for pkg in required_by_installed: - if pkg not in context.to_remove: - if pkg in context.pkgs_data: - dep_deps = context.pkgs_data[pkg].get('d') - else: - dep_deps = deps_nodata_deps.get(pkg) + if name in context.aur_to_update: + del context.aur_to_update[name] - if dep_deps: - source = ', '.join((reverse_to_remove_provided[d] for d in dep_deps if d in reverse_to_remove_provided)) - reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source if source else '?') - context.to_remove[pkg] = UpgradeRequirement(pkg=ArchPackage(name=pkg, - installed=True, - i18n=self.i18n), - reason=reason) + if name in context.repo_to_update: + del context.repo_to_update[name] - for name in context.to_remove: # upgrading lists - if name in context.pkgs_data: - del context.pkgs_data[name] + removed_size = pacman.get_installed_size([*context.to_remove.keys()]) - if name in context.aur_to_update: - del context.aur_to_update[name] + if removed_size: + for name, size in removed_size.items(): + if size is not None: + req = context.to_remove.get(name) + if req: + req.extra_size = size - if name in context.repo_to_update: - del context.repo_to_update[name] + def _add_to_remove(self, pkgs_to_sync: Set[str], names: Dict[str, Set[str]], context: UpdateRequirementsContext, + to_ignore: Set[str] = None): - removed_size = pacman.get_installed_size([*context.to_remove.keys()]) - - if removed_size: - for name, size in removed_size.items(): - if size is not None: - req = context.to_remove.get(name) - if req: - req.extra_size = size - - def _add_to_remove(self, pkgs_to_sync: Set[str], names: Dict[str, Set[str]], context: UpdateRequirementsContext, to_ignore: Set[str] = None): blacklist = to_ignore if to_ignore else set() blacklist.update(names) @@ -582,20 +685,19 @@ class UpdatesSummarizer: dependents[n] = all_deps else: - self.logger.warning("Package '{}' to sync could not be removed from the transaction context because its data was not loaded") + self.logger.warning(f"Package '{pname}' to sync could not be removed from the transaction context " + f"because its data was not loaded") for n in names: if n in context.pkgs_data: if n not in context.to_remove: depends_on = names.get(n) if depends_on: - reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), ', '.join(depends_on)) + reason = f"{self.i18n['arch.info.depends on'].capitalize()} '{', '.join(depends_on)}'" else: reason = '?' - context.to_remove[n] = UpgradeRequirement(pkg=ArchPackage(name=n, - installed=True, - i18n=self.i18n), + context.to_remove[n] = UpgradeRequirement(pkg=ArchPackage(name=n, installed=True, i18n=self.i18n), reason=reason) all_deps = dependents.get(n) @@ -603,7 +705,8 @@ class UpdatesSummarizer: if all_deps: self._add_to_remove(pkgs_to_sync, {dep: {n} for dep in all_deps}, context, blacklist) else: - self.logger.warning("Package '{}' could not be removed from the transaction context because its data was not loaded") + self.logger.warning(f"Package '{n}' could not be removed from the transaction context because its " + f"data was not loaded") def _fill_dependency_breakage(self, context: UpdateRequirementsContext): if bool(context.arch_config['check_dependency_breakage']) and (context.to_update or context.to_install): @@ -667,9 +770,10 @@ class UpdatesSummarizer: context=context) if cannot_upgrade: + pkgs_available = {*context.to_update.values(), *context.to_install.values()} cannot_upgrade.update(self._add_dependents_as_cannot_upgrade(context=context, names=cannot_upgrade, - pkgs_available={*context.to_update.values(), *context.to_install.values()})) + pkgs_available=pkgs_available)) for p in cannot_upgrade: if p in context.to_update: @@ -696,7 +800,9 @@ class UpdatesSummarizer: tf = time.time() self.logger.info("End: checking dependency breakage. Time: {0:.2f} seconds".format(tf - ti)) - def _add_dependents_as_cannot_upgrade(self, context: UpdateRequirementsContext, names: Iterable[str], pkgs_available: Set[ArchPackage], already_removed: Optional[Set[str]] = None, iteration_level: int = 0) -> Set[str]: + def _add_dependents_as_cannot_upgrade(self, context: UpdateRequirementsContext, names: Iterable[str], + pkgs_available: Set[ArchPackage], already_removed: Optional[Set[str]] = None, + iteration_level: int = 0) -> Set[str]: removed = set() if already_removed is None else already_removed removed.update(names) @@ -718,11 +824,10 @@ class UpdatesSummarizer: to_remove.add(pkg.name) if pkg.name not in context.cannot_upgrade: - reason = "{} {}".format(self.i18n['arch.info.depends on'].capitalize(), p) - context.cannot_upgrade[pkg.name] = UpgradeRequirement(pkg=pkg, - reason=reason, - sorting_priority=iteration_level - 1) - + reason = f"{self.i18n['arch.info.depends on'].capitalize()} {p}" + req = UpgradeRequirement(pkg=pkg, reason=reason, + sorting_priority=iteration_level - 1) + context.cannot_upgrade[pkg.name] = req break if to_remove: @@ -732,7 +837,9 @@ class UpdatesSummarizer: return to_remove - def _add_dependency_breakage(self, pkgname: str, pkgdeps: Optional[Set[str]], provided_versions: Dict[str, Set[str]], cannot_upgrade: Set[str], context: UpdateRequirementsContext): + def _add_dependency_breakage(self, pkgname: str, pkgdeps: Optional[Set[str]], + provided_versions: Dict[str, Set[str]], cannot_upgrade: Set[str], + context: UpdateRequirementsContext): if pkgdeps: for dep in pkgdeps: dep_split = RE_DEP_OPERATORS.split(dep) @@ -750,11 +857,14 @@ class UpdatesSummarizer: for v in versions: try: - if match_required_version(current_version=v, operator=op, required_version=dep_split[1]): + if match_required_version(current_version=v, + operator=op, + required_version=dep_split[1]): version_match = True break except: - self.logger.error("Error when comparing versions {} (provided) and {} (required)".format(v, dep_split[1])) + self.logger.error(f"Error when comparing versions {v} (provided) and " + f"{dep_split[1]} (required)") traceback.print_exc() if not version_match: diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index ecf36639..30617852 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -1,52 +1,52 @@ Australia=Australia Austria=Austria Bangladesh=Bangladesh -Belarus=Belarus -Belgium=Belgium -Brazil=Brazil +Belarus=Biellorussia +Belgium=Belgio +Brazil=Brasile Bulgaria=Bulgaria Canada=Canada -Chile=Chile -China=China +Chile=Cile +China=Cina Costa Rica=Costa Rica -Czech=Czech -Denmark=Denmark +Czech=Cecoslovacchia +Denmark=Danimarca Ecuador=Ecuador -France=France +France=Francia Georgia=Georgia -Germany=Germany -Greece=Greece +Germany=Germania +Greece=Grecia Hong Kong=Hong Kong -Hungary=Hungary -Iceland=Iceland +Hungary=Ungheria +Iceland=Islanda India=India Indonesia=Indonesia Iran=Iran -Italy=Italy -Japan=Japan +Italy=Italia +Japan=Giappone Kenya=Kenya -Netherlands=Netherlands -New Zealand=New Zealand -Norway=Norway -Philippines=Philippines -Poland=Poland -Portugal=Portugal +Netherlands=Olanda +New Zealand=Nuova Zelanda +Norway=Norvegia +Philippines=Filippine +Poland=Polonia +Portugal=Portogallo Russia=Russia Singapore=Singapore South Africa=South Africa -South Korea=South Korea -Spain=Spain -Sweden=Sweden -Switzerland=Switzerland +South Korea=Korea del Sud +Spain=Spagna +Sweden=Svezia +Switzerland=Svizzera Taiwan=Taiwan -Thailand=Thailand -Turkey=Turkey -Ukraine=Ukraine -United Kingdom=United Kingdom -United States=United States -action.backup.error.create=It was not possible to generate a new system copy -action.backup.error.delete=It was not possible to delete all the old system copies -action.backup.error.proceed=Proceed anyway ? +Thailand=Tailandia +Turkey=Turchia +Ukraine=Ucraina +United Kingdom=Regno Unito +United States=Stati Uniti +action.backup.error.create=Non è stato possibile generare una nuova copia di sistema +action.backup.error.delete=Non è stato possibile eliminare tutte le vecchie copie di sistema +action.backup.error.proceed=Procedere comunque ? action.backup.invalid_mode=Invalid backup mode action.backup.msg=Do you want to generate a system copy before proceeding ? action.backup.substatus.create=Generating a new system backup @@ -461,4 +461,4 @@ welcome=benvenuto window_manage.search_bar.button_tooltip=Click here to search for applications window_manage.search_bar.placeholder=Cerca window_manage.search_bar.tooltip=Digitare e premere INVIO per cercare applicazioni -yes=si \ No newline at end of file +yes=si diff --git a/tests/gems/arch/test_updates.py b/tests/gems/arch/test_updates.py new file mode 100644 index 00000000..fd717fbd --- /dev/null +++ b/tests/gems/arch/test_updates.py @@ -0,0 +1,1228 @@ +from unittest import TestCase +from unittest.mock import patch, Mock, MagicMock + +from bauh import __app_name__ +from bauh.api.abstract.controller import UpgradeRequirement +from bauh.gems.arch.dependencies import DependenciesAnalyser +from bauh.gems.arch.model import ArchPackage +from bauh.gems.arch.updates import UpdatesSummarizer +from bauh.view.util.translation import I18n + + +class UpdatesSummarizerGetUpgradeRequirementsTest(TestCase): + + def setUp(self) -> None: + self.aur_client = MagicMock() + self.aur_client.read_index.return_value = set() + self.deps_analyser = MagicMock() + self._i18n = {"arch.info.conflicts with": "", "arch.update_summary.to_install.dep_conflict": "{} {}", + "arch.info.required by": "", "arch.info.depends on": ""} + self.i18n = I18n(default_key='en_US', default_locale=self._i18n, current_key='en_US', current_locale=self._i18n) + self.summarizer = UpdatesSummarizer(aur_client=self.aur_client, + aur_supported=True, + i18n=self.i18n, + logger=Mock(), + deps_analyser=self.deps_analyser, + watcher=Mock()) + self.config_ = {"automatch_providers": True, + "prefer_repository_provider": True, + "check_dependency_breakage": True} + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_not_return_installed_to_remove_when_conflict_with_installed_version_fails(self, pacman: Mock): + """ + If the newest version o package A conflicts with an installed package named B, but the conflict expression + doesn't match (e.g: A conflict expressions (B <= 0.7), B (0.8)) then B should not be marked as a package + to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=0.8": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=0.8": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"B<=0.7"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.8-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps',): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_remove) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_return_installed_to_remove_when_conflict_with_installed_version_matches(self, pacman: Mock): + """ + If the newest version o package A conflicts with an installed package named B and the conflict expression + matches (e.g: A conflict expressions (B <= 0.7), B (0.7)) then B should be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A==1.0.1": {"A"}, + "B": {"B"}, + "B=0.7": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=0.7": {"B"} + }, + {"B": {"B"}, # provided to remove + "B=0.7": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"B<=0.7"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.7-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="B", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_return_installed_to_remove_when_conflict_with_installed_matches(self, pacman: Mock): + """ + If the newest version o package A conflicts with an installed package named B, + then B should be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A==1.0.1": {"A"}, + "B": {"B"}, + "B=0.7": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=0.7": {"B"} + }, + {"B": {"B"}, # provided to remove + "B=0.7": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"B"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.7-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="B", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_not_return_installed_to_remove_when_conflict_with_is_self_conflict(self, pacman: Mock): + """ + If the newest version o package A conflicts with itself, then A should not be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "C": {"A"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "C": {"A"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"A<=1.0.0"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}, + "C": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version} + pacman.get_installed_size.return_value = {"A": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + self.assertFalse(res.to_remove) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_not_return_installed_to_remove_when_conflict_with_provided_version_fails(self, pacman: Mock): + """ + If the newest version o package A conflicts with a provided package C (by installed package B), + but the conflict expression doesn't match (e.g: A conflict expressions (C <= 0.7), C (0.8)) + then B should not be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B", "C"}, + "B=0.8": {"B", "C"}, + "C": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B", "C"}, + "B=0.8": {"B", "C"}, + "C": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C<=0.7"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.8-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + self.assertFalse(res.to_remove) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_return_installed_package_to_remove_when_conflict_with_provided_matches_version(self, pacman: Mock): + """ + If the newest version o package A conflicts with a provided package C (by installed package B), + and the conflict expression matches (e.g: A conflict expressions (C <= 0.7), C (0.7)) + then B should be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B", "C"}, + "B=0.8": {"B", "C"}, + "C": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B", "C"}, + "B=0.7": {"B", "C"}, + "C": {"B"} + }, + {"B": {"B"}, # provided to remove + "B=0.7": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C<=0.7"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.7-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="B", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_return_installed_package_to_remove_when_conflict_with_provided_matches(self, pacman: Mock): + """ + If the newest version o package A conflicts with a provided package C (by installed package B), + then B should be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B", "C"}, + "B=0.8": {"B", "C"}, + "C": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B", "C"}, + "B=0.7": {"B", "C"}, + "C": {"B"} + }, + {"B": {"B"}, # provided to remove + "B=0.7": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": "community"} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version, "B": "0.7-1"} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="B", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__should_not_return_installed_to_remove_when_conflict_with_provider_is_self_conflict(self, pacman: Mock): + """ + If the newest version o package A conflicts with a provided package C (by A itself), + then A should not be marked as a package to be removed. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "C": {"A"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "C": {"A"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C<=1.0.0"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}, + "C": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"A": pkg_a.version} + pacman.get_installed_size.return_value = {"A": 1} + pacman.map_required_by.return_value = {"A": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + self.assertFalse(res.to_remove) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_cannot_upgrade_when_several_to_upgrade_conflict_with_each_other(self, pacman: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A conflicts with B, and newest version of B conflicts with A, then + both should be returned as "cannot upgrade" + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": pkg_b.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"B"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"A"}, + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps',): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertFalse(res.to_upgrade) + self.assertEqual(2, len(res.cannot_upgrade)) + self.assertIn(UpgradeRequirement(pkg=pkg_a, reason=" 'B'"), res.cannot_upgrade) + self.assertIn(UpgradeRequirement(pkg=pkg_b, reason=" 'A'"), res.cannot_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_cannot_upgrade_when_several_to_upgrade_conflict_with_provided_by_each_other(self, pacman: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A conflicts with C (provided by B), and newest version of B + conflicts with D (provided by A), then both should be returned as "cannot upgrade" + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": pkg_b.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}, + "D": {"A"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"D"}, + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps',): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertFalse(res.to_upgrade) + self.assertEqual(2, len(res.cannot_upgrade)) + self.assertIn(UpgradeRequirement(pkg=pkg_a, reason=" 'B'"), res.cannot_upgrade) + self.assertIn(UpgradeRequirement(pkg=pkg_b, reason=" 'A'"), res.cannot_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_a_package_to_upgrade_conflicts_with_another(self, pacman: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A conflicts with B, then B should be marked as 'to_remove' + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": pkg_b.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"B"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set(), "B": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.cannot_upgrade) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_a_package_to_upgrade_conflicts_with_provided_by_another(self, pacman: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A conflicts with C (provided by B), then B should be marked as 'to_remove' + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + "C": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": pkg_b.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set(), "B": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.cannot_upgrade) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_a_package_to_upgrade_conflicts_with_provided_version_by_another(self, pacman: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A conflicts with specific version of C (provided by B), + then B should be marked as 'to_remove' + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"B"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + "C": {"B"} + } + ] + pacman.map_repositories.return_value = {"A": pkg_a.repository, "B": pkg_b.repository} + pacman.map_updates_data.return_value = {"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"C<=1.1.0"}, + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': pkg_a.name} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.get_installed_size.return_value = {"A": 1, "B": 1} + pacman.map_required_by.return_value = {"A": set(), "B": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.cannot_upgrade) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'A'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.dependencies.pacman") + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_cannot_upgrade_when_several_packages_to_install_conflict_with_each_other(self, *mocks: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A requires a new package C that conflicts D (new dependency of B), + and D conflicts with C, then A and B should be marked as 'cannot upgrade'. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman, pacman_dependencies = mocks[0], mocks[1] + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"C"}, + "D": {"D"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {c: "community" for c in ("A", "B", "C", "D")} + pacman.map_updates_data.side_effect = [{"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': set(), + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': {"C"}, + 'r': pkg_a.repository, + 'des': "A"}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': {"D"}, + 'r': pkg_a.repository, + 'des': "B"} + }, + {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"D"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"}, + "D": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"C"}, + 'p': {"D": {"D"}, + "D=1.1.0": {"D"}}, + 'd': set(), + 'r': "community", + 'des': "D"} + }] + pacman_dependencies.map_updates_data.return_value = {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"D"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"}, + "D": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"C"}, + 'p': {"D": {"D"}, + "D=1.1.0": {"D"}}, + 'd': set(), + 'r': "community", + 'des': "D"} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.map_required_by.return_value = {c: set() for c in ("A", "B", "C", "D")} + self.deps_analyser = DependenciesAnalyser(aur_client=self.aur_client, i18n=self.i18n, logger=Mock()) + self.summarizer.deps_analyser = self.deps_analyser + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed'): + getattr(pacman, method).assert_called() + + pacman_dependencies.map_updates_data.assert_called() + + self.assertFalse(res.to_install) + self.assertFalse(res.to_upgrade) + self.assertEqual(2, len(res.cannot_upgrade)) + self.assertIn(UpgradeRequirement(pkg=pkg_a, reason="'C' 'D'"), res.cannot_upgrade) + self.assertIn(UpgradeRequirement(pkg=pkg_b, reason="'C' 'D'"), res.cannot_upgrade) + + @patch(f"{__app_name__}.gems.arch.dependencies.pacman") + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_cannot_upgrade_when_packages_to_install_conflict_with_version_of_each_other(self, *mocks: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A requires a new package C that conflicts with D's version + (new dependency of B), and D conflicts with C's version, then A and B should be marked as 'cannot upgrade'. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman, pacman_dependencies = mocks[0], mocks[1] + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"C"}, + "D": {"D"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {c: "community" for c in ("A", "B", "C", "D")} + pacman.map_updates_data.side_effect = [{"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': set(), + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': {"C"}, + 'r': pkg_a.repository, + 'des': "A"}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': {"D"}, + 'r': pkg_a.repository, + 'des': "B"} + }, + {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"D<=1.1.0"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"}, + "D": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"C<=1.1.0"}, + 'p': {"D": {"D"}, + "D=1.1.0": {"D"}}, + 'd': set(), + 'r': "community", + 'des': "D"} + }] + pacman_dependencies.map_updates_data.return_value = {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"D<=1.1.0"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"}, + "D": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': {"C<=1.1.0"}, + 'p': {"D": {"D"}, + "D=1.1.0": {"D"}}, + 'd': set(), + 'r': "community", + 'des': "D"} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.map_required_by.return_value = {c: set() for c in ("A", "B", "C", "D")} + self.deps_analyser = DependenciesAnalyser(aur_client=self.aur_client, i18n=self.i18n, logger=Mock()) + self.summarizer.deps_analyser = self.deps_analyser + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed'): + getattr(pacman, method).assert_called() + + pacman_dependencies.map_updates_data.assert_called() + + self.assertFalse(res.to_install) + self.assertFalse(res.to_upgrade) + self.assertEqual(2, len(res.cannot_upgrade)) + self.assertIn(UpgradeRequirement(pkg=pkg_a, reason="'C' 'D'"), res.cannot_upgrade) + self.assertIn(UpgradeRequirement(pkg=pkg_b, reason="'C' 'D'"), res.cannot_upgrade) + + @patch(f"{__app_name__}.gems.arch.dependencies.pacman") + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_to_update_conflicts_with_to_install(self, *mocks: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A requires a new package C that conflicts with B, + then B should be marked as "to_remove" + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman, pacman_dependencies = mocks[0], mocks[1] + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"C"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {c: "community" for c in ("A", "B", "C")} + pacman.map_updates_data.side_effect = [{"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': set(), + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': {"C"}, + 'r': pkg_a.repository, + 'des': "A"}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': "B"} + }, + {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + ] + pacman_dependencies.map_updates_data.return_value = {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.map_required_by.return_value = {c: set() for c in ("A", "B", "C")} + pacman.get_installed_size.return_value = {"A": 1, "B": 1, "C": 1} + self.deps_analyser = DependenciesAnalyser(aur_client=self.aur_client, i18n=self.i18n, logger=Mock()) + self.summarizer.deps_analyser = self.deps_analyser + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', 'get_installed_size', + 'map_required_by'): + getattr(pacman, method).assert_called() + + pacman_dependencies.map_updates_data.assert_called() + + self.assertFalse(res.cannot_upgrade) + + pkg_c = ArchPackage(name="C", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + self.assertEqual([UpgradeRequirement(pkg=pkg_c, required_size=1, extra_size=1, reason=": A")], res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, extra_size=1, reason=" 'C'")], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.dependencies.pacman") + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_to_update_conflicts_with_specific_version_of_install(self, *mocks: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A requires a new package C that conflicts with latest version of B, + then B should be marked as 'to_remove'. + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman, pacman_dependencies = mocks[0], mocks[1] + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"C"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + } + ] + pacman.map_repositories.return_value = {c: "community" for c in ("A", "B", "C")} + pacman.map_updates_data.side_effect = [{"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': set(), + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': {"C"}, + 'r': pkg_a.repository, + 'des': "A"}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': "B"} + }, + {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B<=1.1.0"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + ] + pacman_dependencies.map_updates_data.return_value = {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B<=1.1.0"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version} + pacman.map_required_by.return_value = {c: set() for c in ("A", "B", "C")} + pacman.get_installed_size.return_value = {"A": 1, "B": 1, "C": 1} + self.deps_analyser = DependenciesAnalyser(aur_client=self.aur_client, i18n=self.i18n, logger=Mock()) + self.summarizer.deps_analyser = self.deps_analyser + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', 'get_installed_size', + 'map_required_by'): + getattr(pacman, method).assert_called() + + pacman_dependencies.map_updates_data.assert_called() + + self.assertFalse(res.cannot_upgrade) + + pkg_c = ArchPackage(name="C", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + self.assertEqual([UpgradeRequirement(pkg=pkg_c, required_size=1, extra_size=1, reason=": A")], res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, extra_size=1, reason=" 'C'")], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.dependencies.pacman") + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_as_to_remove_when_to_update_conflicts_with_to_install_and_it_has_deps(self, *mocks: Mock): + """ + Consider package A and B are selected to be upgraded: + + If the newest version o package A requires a new package C that conflicts with B, + then B should be marked as "to_remove" and its dependents as well (package 'D'). + """ + pkg_a = ArchPackage(name="A", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + pkg_b = ArchPackage(name="B", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + + pacman, pacman_dependencies = mocks[0], mocks[1] + pacman.map_provided.side_effect = [{"A": {"A"}, # remote provided + "A=1.1.0": {"A"}, + "B": {"B"}, + "B=1.1.0": {"B"}, + "C": {"C"} + }, + {"A": {"A"}, # provided + "A=1.0.0": {"A"}, + "B": {"B"}, + "B=1.0.0": {"B"}, + "D": {"D"}, + "D=0.7.0": {"D"} + }, + {"B": {"B"}, # provided to remove + "B=1.0.0": {"B"}, + "D": {"D"}, + "D=0.7.0": {"D"} + } + ] + pacman.map_repositories.return_value = {c: "community" for c in ("A", "B", "C", "D")} + pacman.map_updates_data.side_effect = [{"A": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': set(), + 'p': {"A": {"A"}, + "A=1.1.0": {"A"}}, + 'd': {"C"}, + 'r': pkg_a.repository, + 'des': "A"}, + "B": {'ds': 1, + 's': 1, + 'v': pkg_b.latest_version, + 'c': set(), + 'p': {"B": {"B"}, + "B=1.1.0": {"B"}}, + 'd': set(), + 'r': pkg_a.repository, + 'des': "B"} + }, + {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + ] + pacman_dependencies.map_updates_data.return_value = {"C": {'ds': 1, + 's': 1, + 'v': "1.1.0-1", + 'c': {"B"}, + 'p': {"C": {"C"}, + "C=1.1.0": {"C"}}, + 'd': set(), + 'r': "community", + 'des': "C"} + } + pacman_dependencies.map_required_by.return_value = {"B": {"D"}} + + pacman.map_installed.return_value = {"A": pkg_a.version, "B": pkg_b.version, "D": "0.7.0"} + pacman.map_required_by.return_value = {**{c: set() for c in ("A", "C", "D")}, "B": {"D"}} + pacman.map_required_dependencies.return_value = {"D": {"B"}} + pacman.get_installed_size.return_value = {"A": 1, "B": 1, "C": 1, "D": 1} + + self.deps_analyser = DependenciesAnalyser(aur_client=self.aur_client, i18n=self.i18n, logger=Mock()) + self.summarizer.deps_analyser = self.deps_analyser + + res = self.summarizer.summarize(pkgs=[pkg_a, pkg_b], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', 'get_installed_size', + 'map_required_by', 'map_required_dependencies'): + getattr(pacman, method).assert_called() + + for method in ('map_updates_data', 'map_required_by'): + getattr(pacman_dependencies, method).assert_called() + + self.assertFalse(res.cannot_upgrade) + + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_c = ArchPackage(name="C", version="1.0.0-1", latest_version="1.1.0-1", repository="community") + self.assertEqual([UpgradeRequirement(pkg=pkg_c, required_size=1, extra_size=1, reason=": A")], res.to_install) + + self.assertEqual(2, len(res.to_remove)) + self.assertIn(UpgradeRequirement(pkg=pkg_b, extra_size=1, reason=" 'C'"), res.to_remove) + + pkg_d = ArchPackage(name="D", repository="community", version="0.7.0-1", latest_version="0.7.0-1") + self.assertIn(UpgradeRequirement(pkg=pkg_d, extra_size=1, reason=" 'B=1.0.0'"), res.to_remove)