diff --git a/AppImage/scripts/oci_menu_bridge.py b/AppImage/scripts/oci_menu_bridge.py index c63943e6..f48ecf0a 100644 --- a/AppImage/scripts/oci_menu_bridge.py +++ b/AppImage/scripts/oci_menu_bridge.py @@ -74,19 +74,19 @@ def check_applications(module, rows, report): module.check_app(vmid, app['id'], force=True, notify=False) pool = ThreadPoolExecutor(max_workers=_CHECK_WORKERS, thread_name_prefix='oci-check') - failed = False + failed = [] try: - futures = [pool.submit(check_one, row) for row in rows] + futures = {pool.submit(check_one, row): row['vmid'] for row in rows} for completed, future in enumerate(as_completed(futures), 1): try: future.result() except Exception: - failed = True # One failure never stops the other applications. + failed.append(futures[future]) # One failure never stops the other applications. report({'completed': completed, 'total': len(rows)}) finally: stopped.set() pool.shutdown(wait=True, cancel_futures=True) - report({'error': 'check_failed'} if failed else {'done': True}) + report({'done': True, 'failed': sorted(failed)}) class _Handler(socketserver.StreamRequestHandler): diff --git a/lang/de.json b/lang/de.json index ced51402..d722320e 100644 --- a/lang/de.json +++ b/lang/de.json @@ -5940,6 +5940,7 @@ "Snippets — hook scripts / config": "Snippets – Hook-Skripte/Konfiguration", "SoC-integrated GPU: tight coupling with other SoC components": "SoC-integrierte GPU: enge Kopplung mit anderen SoC-Komponenten", "Some DKMS removals reported errors; final verification will determine the result.": "Bei einigen DKMS-Entfernungen wurden Fehler gemeldet. Über das Ergebnis entscheidet die abschließende Prüfung.", + "Some applications could not be checked ({count}). Try again later.": "Einige Anwendungen konnten nicht geprüft werden ({count}). Versuchen Sie es später erneut.", "Some changes require a reboot to take effect. Do you want to restart now?": "Einige Änderungen erfordern einen Neustart, damit sie wirksam werden. Möchten Sie jetzt neu starten?", "Some essential Proxmox packages may not have been installed": "Einige wichtige Proxmox-Pakete wurden möglicherweise nicht installiert", "Some log2ram files may still exist. Manual cleanup may be required.": "Möglicherweise sind noch einige Log2RAM-Dateien vorhanden. Möglicherweise ist eine manuelle Bereinigung erforderlich.", @@ -6794,6 +6795,7 @@ "The tmpfs mounts of the container differ from the saved record": "Die tmpfs Halterungen des Containers unterscheiden sich vom gespeicherten Datensatz", "The tmpfs path or size is outside the supported profile": "Der Pfad oder die Größe von tmpfs befindet sich außerhalb des unterstützten Profils", "The translated recipe changed during the preparation": "Das übersetzte Rezept änderte sich während der Vorbereitung", + "The update check could not be completed. Try again later.": "Die Update-Prüfung konnte nicht abgeschlossen werden. Versuchen Sie es später erneut.", "The value contains an unsupported character": "Der Wert enthält ein nicht unterstütztes Zeichen", "The value must be a number:": "Der Wert muss eine Zahl sein:", "The values do not match. Enter them again.": "Die Werte stimmen nicht überein. Betreten Sie sie wieder.", diff --git a/lang/es.json b/lang/es.json index 6f4fe30d..f1948589 100644 --- a/lang/es.json +++ b/lang/es.json @@ -5925,6 +5925,7 @@ "Snippets — hook scripts / config": "Snippets — hook scripts / config", "SoC-integrated GPU: tight coupling with other SoC components": "GPU integrada en SoC: estrecho acoplamiento con otros componentes de SoC", "Some DKMS removals reported errors; final verification will determine the result.": "Algunas eliminaciones de DKMS han devuelto errores; la verificación final determinará el resultado.", + "Some applications could not be checked ({count}). Try again later.": "No se han podido comprobar algunas aplicaciones ({count}). Vuelve a intentarlo más tarde.", "Some changes require a reboot to take effect. Do you want to restart now?": "Algunos cambios requieren un reinicio para que surtan efecto. ¿Quieres reiniciar ahora?", "Some essential Proxmox packages may not have been installed": "Es posible que algunos paquetes esenciales de Proxmox no se hayan instalado", "Some log2ram files may still exist. Manual cleanup may be required.": "Es posible que aún existan algunos archivos log2ram. Es posible que se requiera una limpieza manual.", @@ -6770,6 +6771,7 @@ "The tmpfs mounts of the container differ from the saved record": "Los montajes tmpfs del contenedor difieren del registro guardado", "The tmpfs path or size is outside the supported profile": "El camino o tamaño de tmpfs está fuera del perfil soportado", "The translated recipe changed during the preparation": "La receta traducida cambió durante la preparación", + "The update check could not be completed. Try again later.": "La búsqueda de actualizaciones no ha podido terminar. Vuelve a intentarlo más tarde.", "The value contains an unsupported character": "El valor contiene un carácter no admitido", "The value must be a number:": "El valor debe ser un número:", "The values do not match. Enter them again.": "Los valores no coinciden. Vuelve a introducirlos.", diff --git a/lang/fr.json b/lang/fr.json index 78f69575..d5e55281 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -5940,6 +5940,7 @@ "Snippets — hook scripts / config": "Extraits - scripts de hook / configuration", "SoC-integrated GPU: tight coupling with other SoC components": "GPU intégré au SoC : couplage étroit avec d'autres composants du SoC", "Some DKMS removals reported errors; final verification will determine the result.": "Certaines suppressions de DKMS ont signalé des erreurs ;la vérification finale déterminera le résultat.", + "Some applications could not be checked ({count}). Try again later.": "Certaines applications n'ont pas pu être vérifiées ({count}). Réessayez plus tard.", "Some changes require a reboot to take effect. Do you want to restart now?": "Certaines modifications nécessitent un redémarrage pour prendre effet. Voulez-vous redémarrer maintenant ?", "Some essential Proxmox packages may not have been installed": "Certains packages Proxmox essentiels n'ont peut-être pas été installés", "Some log2ram files may still exist. Manual cleanup may be required.": "Certains fichiers log2ram peuvent encore exister. Un nettoyage manuel peut être nécessaire.", @@ -6793,6 +6794,7 @@ "The tmpfs mounts of the container differ from the saved record": "Les supports tmpfs du conteneur diffèrent de l'enregistrement enregistré", "The tmpfs path or size is outside the supported profile": "Le chemin ou la taille de tmpfs est en dehors du profil pris en charge", "The translated recipe changed during the preparation": "La recette traduite a changé pendant la préparation", + "The update check could not be completed. Try again later.": "La recherche de mises à jour n'a pas pu se terminer. Réessayez plus tard.", "The value contains an unsupported character": "La valeur contient un caractère non pris en charge", "The value must be a number:": "La valeur doit être un nombre :", "The values do not match. Enter them again.": "Les valeurs ne correspondent pas. Entrez encore.", diff --git a/lang/it.json b/lang/it.json index 93e80b8e..6f894e4d 100644 --- a/lang/it.json +++ b/lang/it.json @@ -5940,6 +5940,7 @@ "Snippets — hook scripts / config": "Snippet: script di hook/config", "SoC-integrated GPU: tight coupling with other SoC components": "GPU integrata nel SoC: stretto accoppiamento con altri componenti SoC", "Some DKMS removals reported errors; final verification will determine the result.": "Alcune rimozioni DKMS hanno riportato errori; la verifica finale determinerà il risultato.", + "Some applications could not be checked ({count}). Try again later.": "Non è stato possibile controllare alcune applicazioni ({count}). Riprova più tardi.", "Some changes require a reboot to take effect. Do you want to restart now?": "Alcune modifiche richiedono un riavvio per avere effetto. Vuoi riavviare adesso?", "Some essential Proxmox packages may not have been installed": "Alcuni pacchetti Proxmox essenziali potrebbero non essere stati installati", "Some log2ram files may still exist. Manual cleanup may be required.": "Alcuni file log2ram potrebbero ancora esistere. Potrebbe essere necessaria la pulizia manuale.", @@ -6794,6 +6795,7 @@ "The tmpfs mounts of the container differ from the saved record": "I mount tmpfs del container differiscono dal record salvato", "The tmpfs path or size is outside the supported profile": "Il percorso o la dimensione di tmpfs non rientra nel profilo supportato", "The translated recipe changed during the preparation": "La ricetta tradotta è cambiata durante la preparazione", + "The update check could not be completed. Try again later.": "La ricerca degli aggiornamenti non è stata completata. Riprova più tardi.", "The value contains an unsupported character": "Il valore contiene un carattere non supportato", "The value must be a number:": "Il valore deve essere un numero:", "The values do not match. Enter them again.": "I valori non corrispondono. Inserirli di nuovo.", diff --git a/lang/pt.json b/lang/pt.json index 8b98cda9..016c2d57 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -5940,6 +5940,7 @@ "Snippets — hook scripts / config": "Snippets – scripts/configuração de gancho", "SoC-integrated GPU: tight coupling with other SoC components": "GPU integrada ao SoC: forte acoplamento com outros componentes do SoC", "Some DKMS removals reported errors; final verification will determine the result.": "Algumas remoções de DKMS relataram erros;a verificação final determinará o resultado.", + "Some applications could not be checked ({count}). Try again later.": "Não foi possível verificar algumas aplicações ({count}). Tente novamente mais tarde.", "Some changes require a reboot to take effect. Do you want to restart now?": "Algumas alterações exigem uma reinicialização para entrar em vigor. Quer reiniciar agora?", "Some essential Proxmox packages may not have been installed": "Alguns pacotes essenciais do Proxmox podem não ter sido instalados", "Some log2ram files may still exist. Manual cleanup may be required.": "Alguns arquivos log2ram ainda podem existir. A limpeza manual pode ser necessária.", @@ -6794,6 +6795,7 @@ "The tmpfs mounts of the container differ from the saved record": "As montagens tmpfs do recipiente diferem do registo gravado", "The tmpfs path or size is outside the supported profile": "O caminho ou tamanho do tmpfs está fora do perfil suportado", "The translated recipe changed during the preparation": "A receita traduzida mudou durante a preparação", + "The update check could not be completed. Try again later.": "A pesquisa de atualizações não pôde ser concluída. Tente novamente mais tarde.", "The value contains an unsupported character": "O valor contém um caracter não suportado", "The value must be a number:": "O valor deve ser um número:", "The values do not match. Enter them again.": "Os valores não correspondem. Entra outra vez.", diff --git a/lang/sk.json b/lang/sk.json index e813b979..f5fe409b 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -5942,6 +5942,7 @@ "Snippets — hook scripts / config": "Snippets — hook skripty / konfigurácia", "SoC-integrated GPU: tight coupling with other SoC components": "GPU integrovaná v SoC: úzke prepojenie s ďalšími súčasťami SoC", "Some DKMS removals reported errors; final verification will determine the result.": "Niektoré odstránenia DKMS hlásili chyby;konečné overenie určí výsledok.", + "Some applications could not be checked ({count}). Try again later.": "Niektoré aplikácie sa nepodarilo skontrolovať ({count}). Skúste to znova neskôr.", "Some changes require a reboot to take effect. Do you want to restart now?": "Niektoré zmeny sa prejavia až po reštarte. Chcete reštartovať teraz?", "Some essential Proxmox packages may not have been installed": "Niektoré základné Proxmox balíky sa možno nenainštalovali", "Some log2ram files may still exist. Manual cleanup may be required.": "Niektoré súbory log2ram môžu stále existovať. Možno bude potrebné ručné čistenie.", @@ -6795,6 +6796,7 @@ "The tmpfs mounts of the container differ from the saved record": "Tmpfs držiaky kontajnera sa líšia od uloženého záznamu", "The tmpfs path or size is outside the supported profile": "Tmpfs cesta alebo veľkosť je mimo podporovaný profil", "The translated recipe changed during the preparation": "Preložený recept sa počas prípravy zmenil", + "The update check could not be completed. Try again later.": "Kontrolu aktualizácií sa nepodarilo dokončiť. Skúste to znova neskôr.", "The value contains an unsupported character": "Hodnota obsahuje nepodporovaný znak", "The value must be a number:": "Hodnota musí byť číslo:", "The values do not match. Enter them again.": "Hodnoty sa nezhodujú. Zadajte ich znova.", diff --git a/lang/sv.json b/lang/sv.json index 047b2161..ebb9d1ac 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -5940,6 +5940,7 @@ "Snippets — hook scripts / config": "Snippets — krokskript / config", "SoC-integrated GPU: tight coupling with other SoC components": "SoC-integrerad GPU: tät koppling med andra SoC-komponenter", "Some DKMS removals reported errors; final verification will determine the result.": "Vissa DKMS-borttagningar rapporterade fel;slutlig verifiering kommer att avgöra resultatet.", + "Some applications could not be checked ({count}). Try again later.": "Vissa program kunde inte kontrolleras ({count}). Försök igen senare.", "Some changes require a reboot to take effect. Do you want to restart now?": "Vissa ändringar kräver en omstart för att träda i kraft. Vill du starta om nu?", "Some essential Proxmox packages may not have been installed": "Vissa viktiga Proxmox-paket kanske inte har installerats", "Some log2ram files may still exist. Manual cleanup may be required.": "Vissa log2ram-filer kan fortfarande existera. Manuell rengöring kan krävas.", @@ -6794,6 +6795,7 @@ "The tmpfs mounts of the container differ from the saved record": "Tmpfs montage av behållaren skiljer sig från den sparade rekordet", "The tmpfs path or size is outside the supported profile": "Tmpfs-banan eller storleken ligger utanför den stödda profilen", "The translated recipe changed during the preparation": "Det översatta receptet ändrades under förberedelsen", + "The update check could not be completed. Try again later.": "Uppdateringskontrollen kunde inte slutföras. Försök igen senare.", "The value contains an unsupported character": "Värdet innehåller en ostödd karaktär", "The value must be a number:": "Värdet måste vara ett tal:", "The values do not match. Enter them again.": "Värdena matchar inte. Ange dem igen.", diff --git a/oci/remote/oci_update_check.py b/oci/remote/oci_update_check.py index 66dceee7..3d2faa9b 100644 --- a/oci/remote/oci_update_check.py +++ b/oci/remote/oci_update_check.py @@ -95,13 +95,17 @@ def snapshot(root=instances.ROOT, configs=Path('/etc/pve/lxc')): return applications -def cached(root=instances.ROOT, configs=Path('/etc/pve/lxc'), now=None, apps=APPS): - """Read the Monitor's sidecars; obey its primary-container stack policy.""" +def cached(root=instances.ROOT, configs=Path('/etc/pve/lxc'), now=None, apps=APPS, failed=()): + """Read the Monitor's sidecars; obey its primary-container stack policy. + An application whose last check failed keeps no older result.""" inventory = snapshot(root, configs) result = {'checked': False, 'available': [], 'unknown': 0, 'applications': len(inventory)} instant = time.time() if now is None else now for app in inventory: try: + if app['vmid'] in failed: + result['checked'] = True + raise ValueError('Check failed') if not app['complete'] or not all(member['ready'] for member in app['members']): raise ValueError('Incomplete installation') main = next(member for member in app['members'] if member['vmid'] == app['vmid']) @@ -134,9 +138,15 @@ def cached(root=instances.ROOT, configs=Path('/etc/pve/lxc'), now=None, apps=APP def monitor_available(path=SOCKET): try: parent, endpoint = path.parent.lstat(), path.lstat() - return (stat.S_ISDIR(parent.st_mode) and parent.st_uid == 0 + if not (stat.S_ISDIR(parent.st_mode) and parent.st_uid == 0 and not parent.st_mode & 0o077 and stat.S_ISSOCK(endpoint.st_mode) - and endpoint.st_uid == 0 and not endpoint.st_mode & 0o077) + and endpoint.st_uid == 0 and not endpoint.st_mode & 0o077): + return False + # A Monitor that stopped without cleaning up leaves the socket behind. + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as probe: + probe.settimeout(0.5) + probe.connect(str(path)) + return True except OSError: return False @@ -167,7 +177,10 @@ def check(root=instances.ROOT, configs=Path('/etc/pve/lxc'), progress=None, path raise RuntimeError('Monitor check incomplete') event = json.loads(line) if event.get('done') is True and completed == len(rows): - return + failed = event.get('failed', []) + if not isinstance(failed, list) or any(type(vmid) is not int for vmid in failed): + raise RuntimeError('Monitor check incomplete') + return failed done, count = event.get('completed'), event.get('total') if (type(done) is not int or type(count) is not int or count != len(rows) or not completed <= done <= count or done < 0): @@ -177,14 +190,18 @@ def check(root=instances.ROOT, configs=Path('/etc/pve/lxc'), progress=None, path progress(skipped + completed, total) -def emit_progress(completed, total): - message = (json.dumps({'completed': completed, 'total': total}) + '\n').encode() +def emit(event): + message = (json.dumps(event) + '\n').encode() try: os.write(sys.stdout.fileno(), message) except (BlockingIOError, BrokenPipeError): pass +def emit_progress(completed, total): + emit({'completed': completed, 'total': total}) + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--root', type=Path, default=instances.ROOT) @@ -194,7 +211,9 @@ def main(): try: if args.progress: os.set_blocking(sys.stdout.fileno(), False) - check(args.root, args.configs, progress=emit_progress if args.progress else None) + failed = check(args.root, args.configs, progress=emit_progress if args.progress else None) + if args.progress and failed: + emit({'failed': failed}) return 0 except (OSError, ValueError, KeyError, TypeError, RuntimeError): return 1 diff --git a/oci/src/proxmenux_oci/updates.py b/oci/src/proxmenux_oci/updates.py index c01afbd3..aec8714f 100644 --- a/oci/src/proxmenux_oci/updates.py +++ b/oci/src/proxmenux_oci/updates.py @@ -20,6 +20,7 @@ class UpdateCheck: self.backend = importlib.import_module('oci_update_check') self.process = None self.failed = False + self.failed_applications = set() self.completed = self.total = 0 self._progress_buffer = b'' self._shown_progress = (0, 0) @@ -52,6 +53,9 @@ class UpdateCheck: line, self._progress_buffer = self._progress_buffer.split(b'\n', 1) try: data = json.loads(line) + if isinstance(data.get('failed'), list): + self.failed_applications = {vmid for vmid in data['failed'] if type(vmid) is int} + continue completed, total = data['completed'], data['total'] if type(completed) is int and type(total) is int and 0 <= completed <= total: self.completed, self.total = completed, total @@ -77,7 +81,7 @@ class UpdateCheck: self._close_output() self.process = None try: - state = self.backend.cached() + state = self.backend.cached(failed=self.failed_applications) except (OSError, ValueError, KeyError, TypeError): state = {'checked': False, 'available': [], 'unknown': 0, 'applications': 0} if self.failed: @@ -93,6 +97,7 @@ class UpdateCheck: raise ValueError(translate('The ProxMenux Monitor update checker is not available. Start or update ProxMenux Monitor and try again.')) state = self.status() self.failed = False + self.failed_applications = set() self.completed, self.total = 0, state['applications'] self._progress_buffer = b'' self.process = subprocess.Popen( @@ -142,7 +147,14 @@ class UpdateCheck: return f"{translate('Updates available')} ({count})" if count else translate('Check for updates') def notice(self, state): - return '' # Keep the menu heading clear after a check. + if self.active: + return '' + if self.failed: + return translate('The update check could not be completed. Try again later.') + if self.failed_applications: + return translate('Some applications could not be checked ({count}). Try again later.').format( + count=len(self.failed_applications)) + return '' def select_updates(project, ui, available): diff --git a/oci/tests/test_update_check.py b/oci/tests/test_update_check.py index 0bff26eb..f8141c11 100644 --- a/oci/tests/test_update_check.py +++ b/oci/tests/test_update_check.py @@ -177,7 +177,8 @@ class UpdateCheckTests(unittest.TestCase): def test_readonly_monitor_availability_rejects_non_private_endpoints(self): path = Mock(); path.parent.lstat.return_value = SimpleNamespace(st_mode=stat.S_IFDIR | 0o700, st_uid=0) path.lstat.return_value = SimpleNamespace(st_mode=stat.S_IFSOCK | 0o600, st_uid=0) - self.assertTrue(check.monitor_available(path)) + with patch.object(check.socket, 'socket'): + self.assertTrue(check.monitor_available(path)) path.lstat.return_value.st_mode |= 0o066 self.assertFalse(check.monitor_available(path)) path.lstat.return_value.st_mode = stat.S_IFREG | 0o600 @@ -186,6 +187,30 @@ class UpdateCheckTests(unittest.TestCase): path.parent.lstat.return_value.st_uid = 1000 self.assertFalse(check.monitor_available(path)) + def test_socket_left_by_a_stopped_monitor_is_not_available(self): + folder = Path(tempfile.mkdtemp(dir='/tmp')); self.addCleanup(lambda: __import__('shutil').rmtree(folder)) + folder.chmod(0o700); path = folder / 'check.sock' + listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); listener.bind(str(path)); path.chmod(0o600) + listener.listen(1) + owner = SimpleNamespace(st_uid=0) + real_lstat = Path.lstat + def lstat(self): + value = real_lstat(self) + return SimpleNamespace(st_mode=value.st_mode, st_uid=owner.st_uid) + with patch.object(Path, 'lstat', lstat): + self.assertTrue(check.monitor_available(path)) + listener.close() # The socket file stays, nothing listens. + self.assertTrue(path.exists()) + self.assertFalse(check.monitor_available(path)) + + def test_failed_application_keeps_no_older_result(self): + self.record(); self.sidecar(update_available=True) + self.assertEqual([r['vmid'] for r in self.status()['available']], [110]) + state = self.status(failed={110}) + self.assertEqual(state['available'], []) + self.assertEqual(state['unknown'], 1) + self.assertTrue(state['checked']) + def test_full_or_closed_progress_pipe_does_not_abort_client(self): for error in (BlockingIOError(), BrokenPipeError()): with patch.object(check.os, 'write', side_effect=error), patch.object(check.sys, 'stdout'): @@ -205,7 +230,7 @@ class BridgeTests(unittest.TestCase): module = self.module(); events = [] bridge.check_applications(module, [{'vmid': 110, 'installation_id': 'id-110'}], events.append) module.check_app.assert_called_once_with(110, 'native-app', force=True, notify=False) - self.assertEqual(events, [{'completed': 0, 'total': 1}, {'completed': 1, 'total': 1}, {'done': True}]) + self.assertEqual(events, [{'completed': 0, 'total': 1}, {'completed': 1, 'total': 1}, {'done': True, 'failed': []}]) def test_replaced_guest_pending_operation_and_secondary_are_not_queried(self): for changes, secondary in (({'installation_id': 'replacement'}, False), @@ -236,10 +261,10 @@ class BridgeTests(unittest.TestCase): bridge.check_applications(module, [{'vmid': 110, 'installation_id': 'id-110'}, {'vmid': 111, 'installation_id': 'id-111'}], report) self.assertEqual(events, [{'completed': 0, 'total': 2}, {'completed': 1, 'total': 2}, - {'completed': 2, 'total': 2}, {'done': True}]) + {'completed': 2, 'total': 2}, {'done': True, 'failed': []}]) self.assertEqual(module.check_app.call_count, 2) - def test_unexpected_failure_does_not_skip_other_apps_or_report_success(self): + def test_unexpected_failure_does_not_skip_other_apps_and_names_the_failed_app(self): module = self.module(); events = [] module._read_oci_record.side_effect = lambda vmid: { 'vmid': vmid, 'installation_id': f'id-{vmid}', 'status': 'installed'} @@ -248,7 +273,7 @@ class BridgeTests(unittest.TestCase): {'vmid': 111, 'installation_id': 'id-111'}], events.append) self.assertEqual(module.check_app.call_count, 2) self.assertEqual(events[-2], {'completed': 2, 'total': 2}) - self.assertEqual(events[-1], {'error': 'check_failed'}) + self.assertEqual(events[-1], {'done': True, 'failed': [110]}) def test_requests_cannot_specify_commands_images_paths_or_duplicate_guests(self): valid = {'vmid': 110, 'installation_id': 'id'} diff --git a/oci/tests/test_update_menu.py b/oci/tests/test_update_menu.py index e1acb56c..9f9d4348 100644 --- a/oci/tests/test_update_menu.py +++ b/oci/tests/test_update_menu.py @@ -134,7 +134,7 @@ class StatusTests(unittest.TestCase): state = checker.status() self.assertEqual(checker.label(state), 'Check for updates') self.assertEqual(state['available'], []) - self.assertEqual(checker.notice(state), '') + self.assertEqual(checker.notice(state), 'The update check could not be completed. Try again later.') def test_finished_worker_keeps_the_final_redraw_pending_until_status_is_consumed(self): checker = self.checker(); checker.completed = checker.total = 2 @@ -163,6 +163,29 @@ class StatusTests(unittest.TestCase): self.assertEqual(args.kwargs['stdout'], subprocess.PIPE) blocking.assert_called_once_with(checker.process.stdout.fileno(), False) + def test_failed_applications_are_reported_and_cleared_by_a_new_check(self): + checker = self.checker(); read, write = os.pipe() + os.write(write, b'{"completed": 2, "total": 2}\n{"failed": [110]}\n'); os.close(write) + os.set_blocking(read, False) + checker.process = Mock(stdout=Mock(fileno=Mock(return_value=read))) + checker.process.poll.return_value = checker.process.wait.return_value = 0 + state = {'checked': True, 'available': [], 'unknown': 1, 'applications': 2} + with patch.object(checker.backend, 'cached', return_value=state) as cached: + final = checker.status() + os.close(read) + self.assertEqual(cached.call_args.kwargs['failed'], {110}) + self.assertEqual(checker.notice(final), + 'Some applications could not be checked (1). Try again later.') + with patch.object(subprocess, 'Popen'), patch.object(updates.os, 'set_blocking'), \ + patch.object(checker.backend, 'monitor_available', return_value=True), \ + patch.object(checker.backend, 'cached', return_value=state): + checker.start() + self.assertEqual(checker.failed_applications, set()) + + def test_a_failed_check_is_reported_in_the_menu(self): + checker = self.checker(); checker.failed = True + self.assertEqual(checker.notice({}), 'The update check could not be completed. Try again later.') + def test_missing_monitor_never_starts_a_fallback_checker(self): checker = self.checker() with patch.object(checker.backend, 'monitor_available', return_value=False), \