diff --git a/CHANGELOG.md b/CHANGELOG.md index 9567ba74..815aaebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.11] +### New system requirements +- **python-dateutil**: better Python library for date handling (install the equivalent package for your Linux distribution before upgrading bauh) + ### Improvements - Arch + - AUR: upgrade checking now considers modification dates as well (needed because not all AUR packages follow versioning standards) - the task responsible for generating a local AUR index is displayed on the initialization dialog ### Fixes - AppImage - some installed applications cannot be launched by their desktop entries (regression from **0.9.10**) [#155](https://github.com/vinifmor/bauh/issues/155). If any of your installed AppImages are affected by this issue, just reinstall them. + ## [0.9.10] 2020-12-11 ### Features - Web diff --git a/README.md b/README.md index e6ed23db..33586c69 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/ #### Debian-based distros - **python3.5** or above - **pip3** -- **python3-requests** - **python-yaml** +- **python3-dateutil** - **python3-pyqt5.qtsvg** +- **python3-requests** - **libqt5svg5** - **qt5dxcb-plugin** - **libappindicator3** (for the **tray mode** in GTK3 desktop environments) @@ -42,6 +43,7 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/ #### Arch-based distros - **python** +- **python-dateutil** - **python-requests** - **python-pip** - **python-pyqt5** diff --git a/bauh/api/abstract/disk.py b/bauh/api/abstract/disk.py index 02de2707..fa2e8413 100644 --- a/bauh/api/abstract/disk.py +++ b/bauh/api/abstract/disk.py @@ -19,11 +19,12 @@ class DiskCacheLoader: """ pass - def fill(self, pkg: SoftwarePackage): + def fill(self, pkg: SoftwarePackage, sync: bool = False): """ fill cached data from the disk of a given package instance If a cache mapping was previously done, then data retrieved will be cached to memory as well. :param pkg: + :param sync: if the package data must be filled synchronously :return: """ pass diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 2b130803..c053a94c 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -4,7 +4,7 @@ import sys import time from io import StringIO from subprocess import PIPE -from typing import List, Tuple, Set, Dict +from typing import List, Tuple, Set, Dict, Optional # default environment variables for subprocesses. from bauh.api.abstract.handler import ProcessWatcher @@ -322,6 +322,11 @@ def check_enabled_services(*names: str) -> Dict[str, bool]: return {s: status[i].strip().lower() == 'enabled' for i, s in enumerate(names) if s} -def execute(cmd: str, shell: bool = False) -> Tuple[int, str]: - p = subprocess.run(args=cmd.split(' ') if not shell else [cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell) +def execute(cmd: str, shell: bool = False, cwd: Optional[str] = None) -> Tuple[int, str]: + p = subprocess.run(args=cmd.split(' ') if not shell else [cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=shell, + cwd=cwd) + return p.returncode, p.stdout.decode() diff --git a/bauh/commons/util.py b/bauh/commons/util.py index ec6bb050..7a202d95 100644 --- a/bauh/commons/util.py +++ b/bauh/commons/util.py @@ -1,4 +1,4 @@ -import collections +from datetime import datetime def deep_update(source: dict, overrides: dict): @@ -26,3 +26,7 @@ def size_to_byte(size: float, unit: str) -> int: final_size = size * 1000000000000000 return int(final_size) + + +def datetime_as_milis(date: datetime) -> int: + return int(round(date.timestamp() * 1000)) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 257b8f99..93bc1734 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -14,6 +14,7 @@ from threading import Thread from typing import List, Set, Type, Tuple, Dict, Iterable, Optional import requests +from dateutil.parser import parse as parse_date from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \ TransactionResult, SoftwareAction @@ -30,6 +31,7 @@ from bauh.commons.category import CategoriesDownloader from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, SimpleProcess +from bauh.commons.util import datetime_as_milis from bauh.commons.view_utils import new_select from bauh.gems.arch import aur, pacman, makepkg, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ @@ -40,7 +42,7 @@ from bauh.gems.arch.config import read_config, get_build_dir from bauh.gems.arch.dependencies import DependenciesAnalyser from bauh.gems.arch.download import MultithreadedDownloadService, ArchDownloadException from bauh.gems.arch.exceptions import PackageNotFoundException, PackageInHoldException -from bauh.gems.arch.mapper import ArchDataMapper +from bauh.gems.arch.mapper import AURDataMapper from bauh.gems.arch.model import ArchPackage from bauh.gems.arch.output import TransactionStatusHandler from bauh.gems.arch.pacman import RE_DEP_OPERATORS @@ -73,7 +75,7 @@ class TransactionContext: missing_deps: List[Tuple[str, str]] = None, installed: Set[str] = None, removed: Dict[str, SoftwarePackage] = None, disk_loader: DiskCacheLoader = None, disk_cache_updater: Thread = None, new_pkg: bool = False, custom_pkgbuild_path: str = None, - pkgs_to_build: Set[str] = None): + pkgs_to_build: Set[str] = None, last_modified: Optional[int] = None): self.name = name self.base = base self.maintainer = maintainer @@ -103,13 +105,14 @@ class TransactionContext: self.custom_pkgbuild_path = custom_pkgbuild_path self.pkgs_to_build = pkgs_to_build self.previous_change_progress = change_progress + self.last_modified = last_modified @classmethod def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: str, handler: ProcessHandler) -> "TransactionContext": return cls(name=pkg.name, base=pkg.get_base_name(), maintainer=pkg.maintainer, repository=pkg.repository, arch_config=arch_config, watcher=handler.watcher, handler=handler, skip_opt_deps=True, change_progress=True, root_password=root_password, dependency=False, - installed=set(), removed={}, new_pkg=not pkg.installed) + installed=set(), removed={}, new_pkg=not pkg.installed, last_modified=pkg.last_modified) def get_base_name(self): return self.base if self.base else self.name @@ -189,7 +192,7 @@ class ArchManager(SoftwareManager): self.aur_cache = context.cache_factory.new() # context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO - self.mapper = ArchDataMapper(http_client=context.http_client, i18n=context.i18n) + self.aur_mapper = AURDataMapper(http_client=context.http_client, i18n=context.i18n, logger=context.logger) self.i18n = context.i18n self.aur_client = AURClient(http_client=context.http_client, logger=context.logger, x86_64=context.is_system_x86_64()) self.dcache_updater = None @@ -334,7 +337,7 @@ class ArchManager(SoftwareManager): pkg = installed_pkgs.get(apidata['Name']) if not pkg: - pkg = self.mapper.map_api_data(apidata, None, self.categories) + pkg = self.aur_mapper.map_api_data(apidata, None, self.categories) pkg.downgrade_enabled = downgrade_enabled if pkg.installed: @@ -342,7 +345,7 @@ class ArchManager(SoftwareManager): else: res.new.append(pkg) - Thread(target=self.mapper.fill_package_build, args=(pkg,), daemon=True).start() + Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True).start() def _search_in_repos_and_fill(self, words: str, disk_loader: DiskCacheLoader, read_installed: Thread, installed: List[ArchPackage], res: SearchResult): repo_search = pacman.search(words) @@ -383,7 +386,7 @@ class ArchManager(SoftwareManager): read_installed.join() aur_installed = {p.name: p for p in installed if p.repository == 'aur'} - downgrade_enabled = git.is_enabled() + downgrade_enabled = git.is_installed() for pkgdata in api_res['results']: self._upgrade_search_result(pkgdata, aur_installed, downgrade_enabled, res, disk_loader) @@ -407,7 +410,7 @@ class ArchManager(SoftwareManager): if pkgsinfo: read_installed.join() aur_installed = {p.name: p for p in installed if p.repository == 'aur'} - downgrade_enabled = git.is_enabled() + downgrade_enabled = git.is_installed() for pkgdata in pkgsinfo: self._upgrade_search_result(pkgdata, aur_installed, downgrade_enabled, res, disk_loader) @@ -452,7 +455,7 @@ class ArchManager(SoftwareManager): def _fill_aur_pkgs(self, aur_pkgs: dict, output: List[ArchPackage], disk_loader: DiskCacheLoader, internet_available: bool, arch_config: dict): - downgrade_enabled = git.is_enabled() + downgrade_enabled = git.is_installed() if internet_available: try: @@ -461,14 +464,18 @@ class ArchManager(SoftwareManager): if pkgsinfo: editable_pkgbuilds = self._read_editable_pkgbuilds() if arch_config['edit_aur_pkgbuild'] is not False else None for pkgdata in pkgsinfo: - pkg = self.mapper.map_api_data(pkgdata, aur_pkgs, self.categories) + pkg = self.aur_mapper.map_api_data(pkgdata, aur_pkgs, self.categories) pkg.downgrade_enabled = downgrade_enabled pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None - if disk_loader: - disk_loader.fill(pkg) - pkg.status = PackageStatus.READY + if pkg.installed: + if disk_loader: + disk_loader.fill(pkg, sync=True) + pkg.update = self._check_aur_package_update(pkg=pkg, + installed_data=aur_pkgs.get(pkg.name, {}), + api_data=pkgdata) + pkg.status = PackageStatus.READY output.append(pkg) return @@ -489,10 +496,24 @@ class ArchManager(SoftwareManager): if disk_loader: disk_loader.fill(pkg) - pkg.status = PackageStatus.READY + pkg.status = PackageStatus.READY output.append(pkg) + def _check_aur_package_update(self, pkg: ArchPackage, installed_data: dict, api_data: dict) -> bool: + if pkg.last_modified is None: # if last_modified is not available, then the install_date will be used instead + install_date = installed_data.get('install_date') + + if install_date: + try: + pkg.install_date = datetime_as_milis(parse_date(install_date)) + except ValueError: + self.logger.error("Could not parse 'install_date' ({}) from AUR package '{}'".format(install_date, pkg.name)) + else: + self.logger.error("AUR package '{}' install_date was not retrieved".format(pkg.name)) + + return self.aur_mapper.check_update(pkg=pkg, last_modified=api_data['LastModified']) + def _fill_repo_updates(self, updates: dict): updates.update(pacman.list_repository_updates()) @@ -620,56 +641,56 @@ class ArchManager(SoftwareManager): context.project_dir = clone_path srcinfo_path = '{}/.SRCINFO'.format(clone_path) - commits = run_cmd("git log", cwd=clone_path) + logs = git.log_shas_and_timestamps(clone_path) context.watcher.change_progress(40) - if commits: - commit_list = re.findall(r'commit (.+)\n', commits) - if commit_list: - if len(commit_list) > 1: - srcfields = {'pkgver', 'pkgrel'} + if not logs: + context.watcher.show_message(title=self.i18n['arch.downgrade.error'], + body=self.i18n['arch.downgrade.impossible'].format( + context.name), + type_=MessageType.ERROR) + return False - commit_found = None - for idx in range(1, len(commit_list)): - commit = commit_list[idx] - with open(srcinfo_path) as f: - pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name ,fields=srcfields) + srcfields = {'pkgver', 'pkgrel', 'epoch'} - reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path) - if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): - context.handler.watcher.print('Could not downgrade anymore. Aborting...') - return False + commit_found, commit_date = None, None + for idx in range(1, len(logs)): + commit, date = logs[idx][0], logs[idx][1] + with open(srcinfo_path) as f: + pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name, fields=srcfields) - if '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == context.get_version(): - # current version found - commit_found = commit - elif commit_found: - context.watcher.change_substatus(self.i18n['arch.downgrade.version_found']) - checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_path) - if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)): - context.watcher.print("Could not rollback to current version's commit") - return False + reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path) + if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): + context.handler.watcher.print('Could not downgrade anymore. Aborting...') + return False - reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_path) - if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): - context.watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found)) - return False + epoch, version, release = pkgsrc.get('epoch'), pkgsrc.get('pkgver'), pkgsrc.get('pkgrel') - break + if epoch: + current_version = '{}:{}-{}'.format(epoch, version, release) + else: + current_version = '{}-{}'.format(version, release) - context.watcher.change_substatus(self.i18n['arch.downgrade.install_older']) - return self._build(context) - else: - context.watcher.show_message(title=self.i18n['arch.downgrade.error'], - body=self.i18n['arch.downgrade.impossible'].format(context.name), - type_=MessageType.ERROR) + if current_version == context.get_version(): + # current version found + commit_found, commit_date = commit, date + elif commit_found: + context.watcher.change_substatus(self.i18n['arch.downgrade.version_found']) + checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_path) + if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)): + context.watcher.print("Could not rollback to current version's commit") return False - context.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.downgrade.no_commits'], - type_=MessageType.ERROR) - return False + reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_path) + if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): + context.watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found)) + return False + break + + context.watcher.change_substatus(self.i18n['arch.downgrade.install_older']) + context.last_modified = commit_date + return self._build(context) finally: if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']: context.handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', context.build_dir]))) @@ -1071,8 +1092,24 @@ class ArchManager(SoftwareManager): if aur_pkgs: watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs'])) + + self.logger.info("Retrieving the 'last_modified' field for each package to upgrade") + pkgs_api_data = self.aur_client.get_info({p.name for p in aur_pkgs}) + + if not pkgs_api_data: + self.logger.warning("Could not retrieve the 'last_modified' fields from the AUR API during the upgrade process") + for pkg in aur_pkgs: watcher.change_substatus("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], pkg.name, pkg.version)) + + if pkgs_api_data: + apidata = [p for p in pkgs_api_data if p.get('Name') == pkg.name] + + if not apidata: + self.logger.warning("AUR API data from package '{}' could not be found".format(pkg.name)) + else: + self.aur_mapper.fill_last_modified(pkg=pkg, api_data=apidata[0]) + context = TransactionContext.gen_context_from(pkg=pkg, arch_config=arch_config, root_password=root_password, handler=handler) context.change_progress = False @@ -1329,11 +1366,15 @@ class ArchManager(SoftwareManager): def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict: if pkg.installed: - t = Thread(target=self.mapper.fill_package_build, args=(pkg,), daemon=True) + t = Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True) t.start() info = pacman.get_info_dict(pkg.name) + if pkg.last_modified: + info['last_modified'] = self._parse_timestamp(ts=pkg.last_modified, + error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified)) + t.join() if pkg.pkgbuild: @@ -1352,11 +1393,17 @@ class ArchManager(SoftwareManager): '05_votes': pkg.votes, '06_package_base': pkg.package_base, '07_maintainer': pkg.maintainer, - '08_first_submitted': pkg.first_submitted, - '09_last_modified': pkg.last_modified, '10_url': pkg.url_download } + if pkg.first_submitted: + info['08_first_submitted'] = self._parse_timestamp(ts=pkg.first_submitted, + error_msg="Could not parse AUR package '{}' 'first_submitted' field".format(pkg.name, pkg.first_submitted)) + + if pkg.last_modified: + info['09_last_modified'] = self._parse_timestamp(ts=pkg.last_modified, + error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified)) + srcinfo = self.aur_client.get_src_info(pkg.name) if srcinfo: @@ -1383,6 +1430,14 @@ class ArchManager(SoftwareManager): return info + def _parse_timestamp(self, ts: int, error_msg: str) -> datetime: + if ts: + try: + return datetime.fromtimestamp(ts) + except ValueError: + if error_msg: + self.logger.warning(error_msg) + def _get_info_repo_pkg(self, pkg: ArchPackage) -> dict: info = pacman.get_info_dict(pkg.name, remote=not pkg.installed) if pkg.installed: @@ -2205,6 +2260,7 @@ class ArchManager(SoftwareManager): cache_map = {context.name: ArchPackage(name=context.name, repository=context.repository, maintainer=pkg_maintainer, + last_modified=context.last_modified, categories=self.categories.get(context.name))} if context.missing_deps: aur_deps = {dep[0] for dep in context.missing_deps if dep[1] == 'aur'} @@ -2344,7 +2400,7 @@ class ArchManager(SoftwareManager): watcher.change_substatus(self.i18n['arch.makepkg.optimizing']) ArchCompilationOptimizer(arch_config=arch_config, i18n=self.i18n, logger=self.context.logger).optimize() - def install(self, pkg: ArchPackage, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher, context: TransactionContext = None) -> TransactionResult: + def install(self, pkg: ArchPackage, root_password: str, disk_loader: Optional[DiskCacheLoader], watcher: ProcessWatcher, context: TransactionContext = None) -> TransactionResult: self.aur_client.clean_caches() if not self._check_action_allowed(pkg, watcher): @@ -2550,7 +2606,7 @@ class ArchManager(SoftwareManager): if not self._is_wget_available(): warnings.append(self.i18n['arch.warning.disabled'].format(bold('wget'))) - if not git.is_enabled(): + if not git.is_installed(): warnings.append(self.i18n['arch.warning.git'].format(bold('git'))) return warnings @@ -2580,7 +2636,7 @@ class ArchManager(SoftwareManager): res = [] for pkg in api_res: if pkg.get('Name') in suggestions: - res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories), suggestions[pkg['Name']])) + res.append(PackageSuggestion(self.aur_mapper.map_api_data(pkg, {}, self.categories), suggestions[pkg['Name']])) self.logger.info("Mapped {} suggestions".format(len(suggestions))) return res diff --git a/bauh/gems/arch/git.py b/bauh/gems/arch/git.py index c52499e5..92387bab 100644 --- a/bauh/gems/arch/git.py +++ b/bauh/gems/arch/git.py @@ -1,10 +1,11 @@ from datetime import datetime -from typing import List +from typing import List, Tuple, Optional +from bauh.commons import system from bauh.commons.system import new_subprocess -def is_enabled() -> bool: +def is_installed() -> bool: try: new_subprocess(['git', '--version']) return True @@ -12,7 +13,7 @@ def is_enabled() -> bool: return False -def list_commits(proj_dir:str) -> List[dict]: +def list_commits(proj_dir: str) -> List[dict]: logs = new_subprocess(['git', 'log', '--date=iso'], cwd=proj_dir).stdout commits, commit = [], {} @@ -27,3 +28,18 @@ def list_commits(proj_dir:str) -> List[dict]: commit = {} return commits + + +def log_shas_and_timestamps(repo_path: str) -> Optional[List[Tuple[str, int]]]: + code, output = system.execute(cmd='git log --format="%H %at"', shell=True, cwd=repo_path) + + if code == 0: + logs = [] + for line in output.strip().split('\n'): + line_strip = line.strip() + + if line_strip: + line_split = line_strip.split(' ') + logs.append((line_split[0].strip(), int(line_split[1].strip()))) + + return logs diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index 00974511..6888985c 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -1,6 +1,7 @@ +import logging import os import traceback -from datetime import datetime +from typing import Optional from colorama import Fore from pkg_resources import parse_version @@ -13,41 +14,56 @@ from bauh.view.util.translation import I18n URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}' -class ArchDataMapper: +class AURDataMapper: - def __init__(self, http_client: HttpClient, i18n: I18n): + def __init__(self, http_client: HttpClient, i18n: I18n, logger: logging.Logger): self.http_client = http_client self.i18n = i18n + self.logger = logger - def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = True): + def fill_last_modified(self, pkg: ArchPackage, api_data: dict): + last_modified = api_data.get('LastModified') + if last_modified is not None and isinstance(last_modified, int): + pkg.last_modified = last_modified + self.logger.info("'last_modified' field ({}) set to package '{}'".format(last_modified, pkg.name)) + else: + self.logger.warning("Could not set the 'last_modified' field ({}) to package '{}'".format(last_modified, pkg.name)) - version = package.get('Version') + def fill_api_data(self, pkg: ArchPackage, api_data: dict, fill_version: bool = True): + pkg.id = api_data.get('ID') + + if not pkg.name: + pkg.name = api_data.get('Name') + + if not pkg.description: + pkg.description = api_data.get('Description') + + pkg.package_base = api_data.get('PackageBase') + pkg.popularity = api_data.get('Popularity') + pkg.votes = api_data.get('NumVotes') + pkg.maintainer = api_data.get('Maintainer') + pkg.url_download = URL_PKG_DOWNLOAD.format(api_data['URLPath']) if api_data.get('URLPath') else None + + if api_data['FirstSubmitted'] and isinstance(api_data['FirstSubmitted'], int): + pkg.first_submitted = api_data['FirstSubmitted'] + + if not pkg.installed: + self.fill_last_modified(pkg=pkg, api_data=api_data) + + version = api_data.get('Version') if version: version = version.split(':') version = version[0] if len(version) == 1 else version[1] - pkg.id = package.get('ID') - pkg.name = package.get('Name') - if fill_version: pkg.version = version pkg.latest_version = version - pkg.description = package.get('Description') - - pkg.package_base = package.get('PackageBase') - pkg.popularity = package.get('Popularity') - pkg.votes = package.get('NumVotes') - pkg.maintainer = package.get('Maintainer') - pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None - pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None - pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None - pkg.update = self.check_update(pkg.version, pkg.latest_version) @staticmethod - def check_update(version: str, latest_version: str) -> bool: - if version and latest_version: + def check_version_update(version: str, latest_version: str) -> bool: + if version and latest_version and version != latest_version: try: ver_epoch, latest_epoch = version.split(':'), latest_version.split(':') @@ -82,8 +98,8 @@ class ArchDataMapper: if res and res.status_code == 200 and res.text: pkg.pkgbuild = res.text - def map_api_data(self, apidata: dict, installed: dict, categories: dict) -> ArchPackage: - data = installed.get(apidata.get('Name')) if installed else None + def map_api_data(self, apidata: dict, pkgs_installed: Optional[dict], categories: dict) -> ArchPackage: + data = pkgs_installed.get(apidata.get('Name')) if pkgs_installed else None app = ArchPackage(name=apidata.get('Name'), installed=bool(data), repository='aur', i18n=self.i18n) app.status = PackageStatus.LOADING_DATA @@ -95,5 +111,26 @@ class ArchDataMapper: app.description = data.get('description') self.fill_api_data(app, apidata, fill_version=not data) - return app + + def check_update(self, pkg: ArchPackage, last_modified: Optional[int]) -> bool: + valid_last_modified = last_modified is not None and isinstance(last_modified, int) + + if not valid_last_modified: + self.logger.warning("'last_modified' timestamp informed for package '{}' is invalid: {}".format(pkg.name, valid_last_modified)) + + pkg_last_modified_ts = pkg.last_modified if pkg.last_modified is not None else pkg.install_date + + if pkg.last_modified is None: + self.logger.warning("AUR package '{}' has no 'last_modified' field set.".format(pkg.name)) + + if pkg.install_date is None: + self.logger.warning("AUR package '{}' has no 'install_date' field set".format(pkg.name)) + self.logger.warning("Update checking for AUR package '{}' will only consider version strings".format(pkg.name)) + else: + self.logger.warning("AUR package {} 'install_date' field will be used for update checking".format(pkg.name)) + + if pkg_last_modified_ts is not None and valid_last_modified and pkg_last_modified_ts < last_modified: + return True + else: + return self.check_version_update(pkg.version, pkg.latest_version) diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index a78f4287..c013d1d3 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -1,12 +1,11 @@ -import datetime -from typing import List, Set +from typing import List, Set, Optional from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction from bauh.commons import resource from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH from bauh.view.util.translation import I18n -CACHED_ATTRS = {'command', 'icon_path', 'repository', 'maintainer', 'desktop_entry', 'categories'} +CACHED_ATTRS = {'command', 'icon_path', 'repository', 'maintainer', 'desktop_entry', 'categories', 'last_modified'} ACTIONS_AUR_ENABLE_PKGBUILD_EDITION = [CustomSoftwareAction(i18n_label_key='arch.action.enable_pkgbuild_edition', i18n_status_key='arch.action.enable_pkgbuild_edition.status', @@ -27,11 +26,11 @@ class ArchPackage(SoftwarePackage): def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None, package_base: str = None, votes: int = None, popularity: float = None, - first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None, + first_submitted: Optional[int] = None, last_modified: Optional[int] = None, maintainer: str = None, url_download: str = None, pkgbuild: str = None, repository: str = None, desktop_entry: str = None, installed: bool = False, srcinfo: dict = None, dependencies: Set[str] = None, categories: List[str] = None, i18n: I18n = None, update_ignored: bool = False, arch: str = None, - pkgbuild_editable: bool = None): + pkgbuild_editable: bool = None, install_date: Optional[int] = None): super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description, installed=installed, categories=categories) @@ -55,6 +54,7 @@ class ArchPackage(SoftwarePackage): self.update_ignored = update_ignored self.view_name = name # name displayed on the view self.pkgbuild_editable = pkgbuild_editable # if the PKGBUILD can be edited by the user (only for AUR) + self.install_date = install_date @staticmethod def disk_cache_path(pkgname: str): diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 34d41d98..4b426e3b 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -15,7 +15,7 @@ RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]') RE_OPTDEPS = re.compile(r'[\w\._\-]+\s*:') RE_DEP_NOTFOUND = re.compile(r'error:.+\'(.+)\'') RE_DEP_OPERATORS = re.compile(r'[<>=]') -RE_INSTALLED_FIELDS = re.compile(r'(Name|Description|Version|Validated By)\s*:\s*(.+)') +RE_INSTALLED_FIELDS = re.compile(r'(Name|Description|Version|Install Date|Validated By)\s*:\s*(.+)') RE_INSTALLED_SIZE = re.compile(r'Installed Size\s*:\s*([0-9,\.]+)\s(\w+)\n?', re.IGNORECASE) RE_DOWNLOAD_SIZE = re.compile(r'Download Size\s*:\s*([0-9,\.]+)\s(\w+)\n?', re.IGNORECASE) RE_UPDATE_REQUIRED_FIELDS = re.compile(r'(\bProvides\b|\bInstalled Size\b|\bConflicts With\b)\s*:\s(.+)\n') @@ -114,9 +114,11 @@ def map_installed(names: Iterable[str] = None) -> dict: # returns a dict with w if field_tuple[0].startswith('N'): current_pkg['name'] = field_tuple[1].strip() elif field_tuple[0].startswith('Ve'): - current_pkg['version'] = field_tuple[1].split(':')[-1].strip() + current_pkg['version'] = field_tuple[1].strip() elif field_tuple[0].startswith('D'): current_pkg['description'] = field_tuple[1].strip() + elif field_tuple[0].startswith('I'): + current_pkg['install_date'] = field_tuple[1].strip() elif field_tuple[0].startswith('Va'): if field_tuple[1].strip().lower() == 'none': pkgs['not_signed'][current_pkg['name']] = current_pkg @@ -449,8 +451,7 @@ def search(words: str) -> Dict[str, dict]: data_split = repo_split[1].split(' ') current['name'] = data_split[0] - version = data_split[1].split(':') - current['version'] = version[0] if len(version) == 1 else version[1] + current['version'] = data_split[1] return found @@ -1010,7 +1011,7 @@ def map_replaces(names: Iterable[str], remote: bool = False) -> Dict[str, Set[st if output: res = {} - latest_name, replaces = None, None + latest_name, replaces = None, None for l in output.split('\n'): if l: diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 09d21cde..78256d86 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -144,6 +144,7 @@ arch.info.install script=script d'instal·lació arch.info.install script.no=no arch.info.installed files=arxius instal·lats arch.info.installed size=mida instal·lat +arch.info.last_modified=última modificació arch.info.license=llicència arch.info.licenses=llicències arch.info.licenses.custom=costum diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 7229962e..ae1ecd98 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -144,6 +144,7 @@ arch.info.install script=Installationsskript arch.info.install script.no=Nein arch.info.installed files=installierten Dateien arch.info.installed size=installierte Größe +arch.info.last_modified=zuletzt bearbeitet arch.info.license=Lizenz arch.info.licenses=Lizenzen arch.info.licenses.custom=Benutzerdefiniert diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index c8da0736..830b05db 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -144,6 +144,7 @@ arch.info.install script=install script arch.info.install script.no=no arch.info.installed files=installed files arch.info.installed size=installed size +arch.info.last_modified=last modified arch.info.license=license arch.info.licenses=licenses arch.info.licenses.custom=custom diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index bfd98360..7a5c2038 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -144,6 +144,7 @@ arch.info.install script=script de instalación arch.info.install script.no=ninguno arch.info.installed files=archivos instalados arch.info.installed size=tamaño da instalación +arch.info.last_modified=última modificación arch.info.license=licencia arch.info.licenses=licencias arch.info.licenses.custom=personalizada diff --git a/bauh/gems/arch/resources/locale/fr b/bauh/gems/arch/resources/locale/fr index e78fecba..2f9201e8 100644 --- a/bauh/gems/arch/resources/locale/fr +++ b/bauh/gems/arch/resources/locale/fr @@ -144,6 +144,7 @@ arch.info.install script=script d'installation arch.info.install script.no=non arch.info.installed files=fichiers installés arch.info.installed size=taille installée +arch.info.last_modified=dernière modification arch.info.license=licence arch.info.licenses=licences arch.info.licenses.custom=personnalisé diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index c40a313d..e018140b 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -144,6 +144,7 @@ arch.info.install script=script di installazione arch.info.install script.no=nessuno arch.info.installed files=file installati arch.info.installed size=taglia installata +arch.info.last_modified=Ultima modifica arch.info.license=licenza arch.info.licenses=licenze arch.info.licenses.custom=costume diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 2dd6ece1..8ed4bcf8 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -143,6 +143,7 @@ arch.info.install script=script de instalação arch.info.install script.no=nenhum arch.info.installed files=arquivos instalados arch.info.installed size=tamanho da instalação +arch.info.last_modified=última modificação arch.info.license=licença arch.info.licenses=licenças arch.info.licenses.custom=personalizada diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 530d0449..5641aa7d 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -144,6 +144,7 @@ arch.info.install script=Скрипт установки arch.info.install script.no=Нет arch.info.installed files=установленные файлы arch.info.installed size=Размер установки +arch.info.last_modified=Последнее изменение arch.info.license=Лицензия arch.info.licenses=Лицензии arch.info.licenses.custom=таможенная diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index f2b99023..ce8290b1 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -144,6 +144,7 @@ arch.info.install script=kurulum betiği arch.info.install script.no=hayır arch.info.installed files=yüklü dosyalar arch.info.installed size=kurulum boyutu +arch.info.last_modified=son değişiklik arch.info.license=lisans arch.info.licenses=lisanslar arch.info.licenses.custom=özel diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 74115bd9..95cedbf2 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -390,6 +390,7 @@ style=estil success=success summary=resum task.download_categories=Download de categories [ {} ] +task.waiting_task=Waiting for {} type=tipus uninstall=desinstal·la uninstalled=desinstal·lada diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 86df0dfb..e2ab4873 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -390,6 +390,7 @@ style=Stil success=success summary=Zusammenfassung task.download_categories=Downloading categories [ {} ] +task.waiting_task=Waiting for {} type=Typ uninstall=Deinstallation uninstalled=deinstalliert diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 3d9e1bf5..62d01c3d 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -391,6 +391,7 @@ style=style success=success summary=summary task.download_categories=Downloading categories [ {} ] +task.waiting_task=Waiting for {} type=type uninstall=uninstall uninstalled=uninstalled diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index d51a49c1..4b5b4372 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -393,6 +393,7 @@ style=estilo success=éxito summary=resumen task.download_categories=Descargando categorías [ {} ] +task.waiting_task=Esperando por {} type=tipo uninstall=desinstalar uninstalled=desinstalada diff --git a/bauh/view/resources/locale/fr b/bauh/view/resources/locale/fr index c61d00e3..696ccd27 100644 --- a/bauh/view/resources/locale/fr +++ b/bauh/view/resources/locale/fr @@ -387,6 +387,7 @@ style=style success=succès summary=résumé task.download_categories=Téléchargement des catégories [ {} ] +task.waiting_task=Waiting for {} type=type uninstall=désinstaller uninstalled=désinstallé diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 2df5a280..915ba01e 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -393,6 +393,7 @@ style=stile success=success summary=riepilogo task.download_categories=Download delle categorie [ {} ] +task.waiting_task=Waiting for {} type=tipe uninstall=Disinstalla uninstalled=disinstallato diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index a4e86aa5..6a6c45cf 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -391,6 +391,7 @@ style=estilo success=sucesso summary=resumo task.download_categories=Baixando categorias [ {} ] +task.waiting_task=Aguardando {} type=tipo uninstall=desinstalar uninstalled=desinstalado diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 7334d122..7d52e122 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -390,6 +390,7 @@ style=Стиль success=success summary=Суммарно task.download_categories=Downloading categories [ {} ] +task.waiting_task=Waiting for {} type=Тип uninstall=Деинсталляция uninstalled=Деинсталлировано diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr index f20ea155..606dab71 100644 --- a/bauh/view/resources/locale/tr +++ b/bauh/view/resources/locale/tr @@ -390,6 +390,7 @@ style=Görünüm biçimi success=başarılı summary=özet task.download_categories=Kategoriler indiriliyor [ {} ] +task.waiting_task=Waiting for {} type=tür uninstall=kaldır uninstalled=kaldırıldı diff --git a/bauh/view/util/disk.py b/bauh/view/util/disk.py index e25ce42c..058b21cf 100644 --- a/bauh/view/util/disk.py +++ b/bauh/view/util/disk.py @@ -23,14 +23,18 @@ class AsyncDiskCacheLoader(Thread, DiskCacheLoader): self.logger = logger self.processed = 0 - def fill(self, pkg: SoftwarePackage): + def fill(self, pkg: SoftwarePackage, sync: bool = False): """ - Adds a package which data must be read from the disk to a queue. + Adds a package which data must be read from the disk to a queue (if not sync) :param pkg: + :param sync: :return: """ if pkg and pkg.supports_disk_cache(): - self.pkgs.append(pkg) + if sync: + self._fill_cached_data(pkg) + else: + self.pkgs.append(pkg) def stop_working(self): self._work = False diff --git a/requirements.txt b/requirements.txt index a02cc7cd..4ec3a96e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pyqt5>=5.12 requests>=2.18 colorama>=0.3.8 pyyaml>=3.13 +python-dateutil>=2.7 diff --git a/tests/gems/arch/test_arch_data_mapper.py b/tests/gems/arch/test_arch_data_mapper.py deleted file mode 100644 index 0f704d88..00000000 --- a/tests/gems/arch/test_arch_data_mapper.py +++ /dev/null @@ -1,45 +0,0 @@ -from unittest import TestCase - -from bauh.gems.arch.mapper import ArchDataMapper - - -class ArchDataMapperTest(TestCase): - - def test_check_update(self): - self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.0.0-2')) - self.assertFalse(ArchDataMapper.check_update('1.0.0-2', '1.0.0-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0-5', '1.0.1-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.1-1', '1.0.0-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.5-5', '1.1.0-2')) - self.assertFalse(ArchDataMapper.check_update('1.1.0-2', '1.0.5-5')) - self.assertTrue(ArchDataMapper.check_update('1.5.0-2', '1.5.1-1')) - self.assertFalse(ArchDataMapper.check_update('1.5.1-1', '1.5.0-2')) - self.assertTrue(ArchDataMapper.check_update('1.5.1-1', '1.5.1-2')) - self.assertTrue(ArchDataMapper.check_update('1.5.1-1', '2.0.0-1')) - self.assertFalse(ArchDataMapper.check_update('2.0.0-1', '1.5.1-1')) - self.assertTrue(ArchDataMapper.check_update('77.0.3865.90-1', '77.0.3865.120-1')) - self.assertTrue(ArchDataMapper.check_update('77.0.3865.90-1', '77.0.3865.90-2')) - self.assertFalse(ArchDataMapper.check_update('77.0.3865.900-1', '77.0.3865.120-1')) - self.assertTrue(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.900-1')) - self.assertFalse(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.90-1')) - self.assertTrue(ArchDataMapper.check_update('77.0.3865.a-1', '77.0.3865.b-1')) - self.assertFalse(ArchDataMapper.check_update('77.0.b.0-1', '77.0.a.1-1')) - self.assertFalse(ArchDataMapper.check_update('r25.e22697c-1', 'r8.19fe011-1')) - self.assertTrue(ArchDataMapper.check_update('0.9.7.RC-9', '0.9.7.RC-10')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1')) - self.assertFalse(ArchDataMapper.check_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3')) - - def test_check_update__versions_with_epics(self): - self.assertTrue(ArchDataMapper.check_update('1.2-1', '1:1.1-1')) - self.assertFalse(ArchDataMapper.check_update('1:1.1-1', '1.2-1')) - - self.assertTrue(ArchDataMapper.check_update('1:1.2-1', '2:0.1-1')) - self.assertFalse(ArchDataMapper.check_update('2:0.1-1', '1:1.2-1')) - - self.assertTrue(ArchDataMapper.check_update('10:1.1-1', '10:1.2-1')) - self.assertFalse(ArchDataMapper.check_update('10:1.2-1', '10:1.2-1')) - - self.assertTrue(ArchDataMapper.check_update('9:1.2-1', '10:0.1-1')) - - self.assertTrue(ArchDataMapper.check_update('9:1.1.1.1-2', '10:0.0')) - self.assertFalse(ArchDataMapper.check_update('10:0.0', '9:1.1.1.1-2')) diff --git a/tests/gems/arch/test_aur_data_mapper.py b/tests/gems/arch/test_aur_data_mapper.py new file mode 100644 index 00000000..6e643f84 --- /dev/null +++ b/tests/gems/arch/test_aur_data_mapper.py @@ -0,0 +1,178 @@ +from unittest import TestCase +from unittest.mock import Mock + +from bauh.gems.arch.mapper import AURDataMapper +from bauh.gems.arch.model import ArchPackage + + +class ArchDataMapperTest(TestCase): + + def test_check_version_update(self): + self.assertTrue(AURDataMapper.check_version_update('1.0.0-1', '1.0.0-2')) + self.assertFalse(AURDataMapper.check_version_update('1.0.0-2', '1.0.0-1')) + self.assertTrue(AURDataMapper.check_version_update('1.0.0-5', '1.0.1-1')) + self.assertFalse(AURDataMapper.check_version_update('1.0.1-1', '1.0.0-1')) + self.assertTrue(AURDataMapper.check_version_update('1.0.5-5', '1.1.0-2')) + self.assertFalse(AURDataMapper.check_version_update('1.1.0-2', '1.0.5-5')) + self.assertTrue(AURDataMapper.check_version_update('1.5.0-2', '1.5.1-1')) + self.assertFalse(AURDataMapper.check_version_update('1.5.1-1', '1.5.0-2')) + self.assertTrue(AURDataMapper.check_version_update('1.5.1-1', '1.5.1-2')) + self.assertTrue(AURDataMapper.check_version_update('1.5.1-1', '2.0.0-1')) + self.assertFalse(AURDataMapper.check_version_update('2.0.0-1', '1.5.1-1')) + self.assertTrue(AURDataMapper.check_version_update('77.0.3865.90-1', '77.0.3865.120-1')) + self.assertTrue(AURDataMapper.check_version_update('77.0.3865.90-1', '77.0.3865.90-2')) + self.assertFalse(AURDataMapper.check_version_update('77.0.3865.900-1', '77.0.3865.120-1')) + self.assertTrue(AURDataMapper.check_version_update('77.0.3865.120-1', '77.0.3865.900-1')) + self.assertFalse(AURDataMapper.check_version_update('77.0.3865.120-1', '77.0.3865.90-1')) + self.assertTrue(AURDataMapper.check_version_update('77.0.3865.a-1', '77.0.3865.b-1')) + self.assertFalse(AURDataMapper.check_version_update('77.0.b.0-1', '77.0.a.1-1')) + self.assertFalse(AURDataMapper.check_version_update('r25.e22697c-1', 'r8.19fe011-1')) + self.assertTrue(AURDataMapper.check_version_update('0.9.7.RC-9', '0.9.7.RC-10')) + self.assertFalse(AURDataMapper.check_version_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1')) + self.assertFalse(AURDataMapper.check_version_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3')) + + def test_check_version_update__versions_with_epics(self): + self.assertTrue(AURDataMapper.check_version_update('1.2-1', '1:1.1-1')) + self.assertFalse(AURDataMapper.check_version_update('1:1.1-1', '1.2-1')) + + self.assertTrue(AURDataMapper.check_version_update('1:1.2-1', '2:0.1-1')) + self.assertFalse(AURDataMapper.check_version_update('2:0.1-1', '1:1.2-1')) + + self.assertTrue(AURDataMapper.check_version_update('10:1.1-1', '10:1.2-1')) + self.assertFalse(AURDataMapper.check_version_update('10:1.2-1', '10:1.2-1')) + + self.assertTrue(AURDataMapper.check_version_update('9:1.2-1', '10:0.1-1')) + + self.assertTrue(AURDataMapper.check_version_update('9:1.1.1.1-2', '10:0.0')) + self.assertFalse(AURDataMapper.check_version_update('10:0.0', '9:1.1.1.1-2')) + + def test_check_update__pkg_no_last_modified_and_same_versions(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.version = '1.0.0' + pkg.latest_version = pkg.version + + self.assertFalse(mapper.check_update(pkg=pkg, last_modified=1608143812)) + + def test_check_update__pkg_no_last_modified_and_latest_version_higher_than_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=1608143812)) + + def test_check_update__pkg_no_last_modified_and_no_install_date_and_version_higher_than_latest_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.install_date = None + pkg.version = '1.1.0' + pkg.latest_version = '1.0.0' + + self.assertFalse(mapper.check_update(pkg=pkg, last_modified=1608143812)) + + def test_check_update__none_last_modified_and_latest_version_higher_than_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=None)) + + def test_check_update__none_last_modified_and_version_equal_latest_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = pkg.version + + self.assertFalse(mapper.check_update(pkg=pkg, last_modified=None)) + + def test_check_update__string_last_modified_and_latest_version_higher_than_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + self.assertTrue(mapper.check_update(pkg=pkg, last_modified='abc')) + + def test_check_update__pkg_last_modified_equal_last_modified_and_version_equal_latest_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = pkg.version + + self.assertFalse(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified)) + + def test_check_update__pkg_last_modified_higher_than_last_modified_and_latest_version_higher_than_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified - 100)) + + def test_check_update__pkg_last_modified_less_than_last_modified_and_version_higher_than_latest_version(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = 1608143812 + pkg.version = '2.0.0' + pkg.latest_version = '1.0.0' + + # in this case, last modified is more relevant than the string version + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified + 100)) + + def test_check_update__pkg_no_last_modified_and_install_date_less_than_last_modified_and_version_higher_than_latest(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.install_date = 1608143812 + pkg.version = '3.0.0' + pkg.latest_version = '2.0.0' + + # in this case, install_date will be considered instead of package's last_modified. + # even that 'version' is higher than 'latest_version', 'last_modified' is greater than 'install_date' + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.install_date + 100)) + + def test_check_update__pkg_no_last_modified_and_install_date_higher_than_last_modified_and_version_equal_latest(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.install_date = 1608143812 + pkg.version = '2.0.0' + pkg.latest_version = pkg.version + + # in this case, install_date will be considered instead of package's last_modified. + # as 'install_date' is higher, only the string versions will be compared + self.assertFalse(mapper.check_update(pkg=pkg, last_modified=pkg.install_date - 100)) + + def test_check_update__pkg_no_last_modified_and_install_date_higher_than_last_modified_and_latest_higher(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.install_date = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + # in this case, install_date will be considered instead of package's last_modified. + # as 'install_date' is higher, only the string versions will be compared + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.install_date - 100)) + + def test_check_update__pkg_no_last_modified_and_install_date_and_no_last_modified_and_latest_higher(self): + mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock()) + pkg = ArchPackage(name='test') + pkg.last_modified = None + pkg.install_date = 1608143812 + pkg.version = '1.0.0' + pkg.latest_version = '1.1.0' + + # in this case, install_date will be considered instead of package's last_modified. + # as 'install_date' is higher, only the string versions will be compared + self.assertTrue(mapper.check_update(pkg=pkg, last_modified=None))