diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 0bea45ac..763ec4b0 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -12,6 +12,7 @@ from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHistory, PackageSuggestion, PackageAction from bauh.api.abstract.view import FormComponent, ViewComponent +from build.lib.bauh.api.abstract.controller import SoftwareManager class SearchResult: @@ -82,6 +83,14 @@ class SoftwareManager(ABC): if pkg.supports_disk_cache() and os.path.exists(pkg.get_disk_cache_path()): shutil.rmtree(pkg.get_disk_cache_path()) + def sort_update_order(self, pkgs: List[SoftwarePackage]) -> List[SoftwarePackage]: + """ + sorts the best order to perform the update of some packages + :param pkgs: + :return: + """ + return pkgs + @abstractmethod def update(self, pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) -> bool: """ diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 557c60e4..c7195895 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -993,3 +993,70 @@ class ArchManager(SoftwareManager): return True, None except: return False, [traceback.format_exc()] + + def sort_update_order(self, pkgs: List[ArchPackage]) -> List[ArchPackage]: + # TODO pegar o 'provides' dos pacotes ? + pkg_deps = {} + + def _add_info(pkg: ArchPackage): + try: + pkg_deps[pkg] = self.aur_client.get_all_dependencies(pkg.name) + except: + pkg_deps[pkg] = None + self.logger.warning("Could not retrieve dependencies for '{}'".format(pkg.name)) + traceback.print_exc() + + threads = [] + for pkg in pkgs: + t = Thread(target=_add_info, args=(pkg, )) + t.start() + threads.append(t) + + for t in threads: + t.join() + + return self._sort_deps(pkg_deps) + + @classmethod + def _sort_deps(cls, pkg_deps: Dict[ArchPackage, Set[str]]) -> List[ArchPackage]: + sorted_names, not_sorted = {}, {} + pkg_map = {} + + # first adding all with no deps: + for pkg, deps in pkg_deps.items(): + if not deps: + sorted_names[pkg.name] = len(sorted_names) + else: + not_sorted[pkg.name] = pkg + + pkg_map[pkg.name] = pkg + + # now adding all that depends on another: + for name, pkg in not_sorted.items(): + cls._add_to_sort(pkg, pkg_deps, sorted_names, not_sorted) + + position_map = {'{}-{}'.format(i, n): pkg_map[n] for n, i in sorted_names.items()} + return [position_map[idx] for idx in sorted(position_map)] + + @classmethod + def _add_to_sort(cls, pkg: ArchPackage, pkg_deps: Dict[ArchPackage, Set[str]], sorted_names: Dict[str, int], not_sorted: Dict[str, ArchPackage]) -> int: + idx = sorted_names.get(pkg.name) + + if idx is not None: + return idx + else: + idx = len(sorted_names) + sorted_names[pkg.name] = idx + + for dep in pkg_deps[pkg]: + dep_idx = sorted_names.get(dep) + + if dep_idx is not None: + idx = dep_idx + 1 + elif dep in not_sorted: # it means the dep is one of the packages to sort, but it not sorted yet + dep_idx = cls._add_to_sort(not_sorted[dep], pkg_deps, sorted_names, not_sorted) + idx = dep_idx + 1 + + sorted_names[pkg.name] = idx + + return sorted_names[pkg.name] diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index c6bd6f6f..5048b9bb 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -419,3 +419,28 @@ class GenericSoftwareManager(SoftwareManager): self.settings_manager = None return res + + def sort_update_order(self, pkgs: List[SoftwarePackage]) -> List[SoftwarePackage]: + by_manager = {} + for pkg in pkgs: + man = self._get_manager_for(pkg) + + if man: + man_pkgs = by_manager.get(man) + + if man_pkgs is None: + man_pkgs = [] + by_manager[man] = man_pkgs + + man_pkgs.append(pkg) + + sorted_list = [] + + if by_manager: + for man, pkgs in by_manager.items(): + ti = time.time() + sorted_list.extend(man.sort_update_order(pkgs)) + tf = time.time() + self.logger.info(man.__class__.__name__ + " took {0:.2f} seconds".format(tf - ti)) + + return sorted_list diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 24d81103..5813a3ff 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -103,19 +103,23 @@ class UpdateSelectedApps(AsyncAction): if self.apps_to_update: updated, updated_types = 0, set() - for app in self.apps_to_update: - name = app.model.name if not RE_VERSION_IN_NAME.findall(app.model.name) else app.model.name.split('version')[0].strip() + self.change_substatus(self.i18n['action.update.status.sorting']) + sorted_pkgs = self.manager.sort_update_order([view.model for view in self.apps_to_update]) - self.change_status('{} {} {}...'.format(self.i18n['manage_window.status.upgrading'], name, app.model.version)) - success = bool(self.manager.update(app.model, self.root_password, self)) + for pkg in sorted_pkgs: + + name = pkg.name if not RE_VERSION_IN_NAME.findall(pkg.name) else pkg.name.split('version')[0].strip() + + self.change_status('{} {} {}...'.format(self.i18n['manage_window.status.upgrading'], name, pkg.version)) + success = bool(self.manager.update(pkg, self.root_password, self)) self.change_substatus('') if not success: break else: updated += 1 - updated_types.add(app.model.__class__) + updated_types.add(pkg.__class__) self.signal_output.emit('\n') self.notify_finished({'success': success, 'updated': updated, 'types': updated_types}) diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 1277d1e6..bb61659e 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -265,4 +265,5 @@ locale.it=italià interval=interval installation=instal·lació download=download -clean=netejar \ No newline at end of file +clean=netejar +action.update.status.sorting=Determinant el millor ordre d’actualització \ No newline at end of file diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 33b6b03a..0a67504c 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -220,4 +220,5 @@ locale.it=italienisch interval=intervall installation=Installation download=download -clean=reinigen \ No newline at end of file +clean=reinigen +action.update.status.sorting=Determining the best update order \ No newline at end of file diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 02f1eee8..556d35d8 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -227,4 +227,5 @@ locale.it=italian interval=interval installation=installation download=download -clean=clean \ No newline at end of file +clean=clean +action.update.status.sorting=Determining the best update order \ No newline at end of file diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index 2abf90c2..4872cb2e 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -268,4 +268,5 @@ locale.it=italiano interval=intervalo installation=instalación download=descarga -clean=limpiar \ No newline at end of file +clean=limpiar +action.update.status.sorting=Determinando el mejor orden de actualización \ No newline at end of file diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 4c599b40..dfa2e970 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -220,4 +220,5 @@ locale.it=italiano interval=intervallo installation=installazione download=download -clean=pulire \ No newline at end of file +clean=pulire +action.update.status.sorting=Sto determinando il miglior ordine di aggiornamento \ No newline at end of file diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index 52798c48..6e61a5d9 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -271,4 +271,5 @@ locale.it=italiano interval=intervalo installation=instalação download=download -clean=limpar \ No newline at end of file +clean=limpar +action.update.status.sorting=Determinando a melhor ordem de atualização \ No newline at end of file diff --git a/tests/gems/arch/controller.py b/tests/gems/arch/controller.py new file mode 100644 index 00000000..09fd521f --- /dev/null +++ b/tests/gems/arch/controller.py @@ -0,0 +1,138 @@ +from unittest import TestCase + +from bauh.gems.arch.controller import ArchManager +from bauh.gems.arch.model import ArchPackage + + +class ArchManagerSortUpdateOrderTest(TestCase): + + def test__sort_deps__not_related_packages(self): + deps = { + ArchPackage(name='google-chrome', package_base='google-chrome'): {'alsa-lib', 'gtk3', 'libcups'}, + ArchPackage(name='git-cola', package_base='git-cola'): {'git', 'python-pyqt5', 'icu qt5-svg'}, + ArchPackage(name='kazam', package_base='kazam'): {'python', 'python-cairo'} + } + + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + def test__sort_deps__all_packages_no_deps(self): + deps = { + ArchPackage(name='xpto', package_base='xpto'): set(), + ArchPackage(name='abc', package_base='abc'): None + } + + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + def test__sort_deps__one_of_three_related(self): + deps = { + ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, + ArchPackage(name='def', package_base='def'): {'jkl'}, + ArchPackage(name='ghi', package_base='ghi'): {} + } + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + ghi = [p for p in sorted_list if p.name == 'ghi'] + self.assertEqual(1, len(ghi)) + + ghi_idx = sorted_list.index(ghi[0]) + + abc = [p for p in sorted_list if p.name == 'abc'] + self.assertEqual(1, len(abc)) + + abc_idx = sorted_list.index(abc[0]) + self.assertGreater(abc_idx, ghi_idx) + + def test__sort_deps__two_of_three_related(self): + """ + dep order = abc -> ghi -> def + expected: def, ghi, abc + """ + deps = { + ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, + ArchPackage(name='def', package_base='def'): {'jkl'}, + ArchPackage(name='ghi', package_base='ghi'): {'def'} + } + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + self.assertEqual(sorted_list[0].name, 'def') + self.assertEqual(sorted_list[1].name, 'ghi') + self.assertEqual(sorted_list[2].name, 'abc') + + def test__sort_deps__two_relying_on_the_same_package(self): + """ + dep order: + abc -> ghi + jkl -> ghi + ghi -> def + def -> mno + expected: def, ghi, (abc | jkl ) + """ + + deps = { + ArchPackage(name='abc', package_base='abc'): {'ghi', 'xpto'}, + ArchPackage(name='def', package_base='def'): {'mno'}, + ArchPackage(name='ghi', package_base='ghi'): {'def'}, + ArchPackage(name='jkl', package_base='jkl'): {'ghi'} + } + + for _ in range(5): # testing n times to see if the same result is produced + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + self.assertEqual(sorted_list[0].name, 'def') + self.assertEqual(sorted_list[1].name, 'ghi') + + self.assertNotEqual(sorted_list[2].name, sorted_list[3].name) + self.assertIn(sorted_list[2].name, {'abc', 'jkl'}) + self.assertIn(sorted_list[3].name, {'abc', 'jkl'}) + + def test__sort_deps__with_cycle(self): + """ + dep order: + abc -> def -> ghi -> jkl -> abc + expected: + """ + deps = { + ArchPackage(name='abc', package_base='abc'): {'def'}, + ArchPackage(name='def', package_base='def'): {'ghi'}, + ArchPackage(name='ghi', package_base='ghi'): {'jkl'}, + ArchPackage(name='jkl', package_base='jkl'): {'abc'} + } + + sorted_list = ArchManager._sort_deps(deps) + self.assertIsInstance(sorted_list, list) + self.assertEqual(len(deps), len(sorted_list)) + + for pkg in sorted_list: + self.assertIn(pkg, deps) + + def test__sort_deps__a_declared_dep_provided_as_a_different_name(self): + pass