return to the installed applications list after each operation

This commit is contained in:
MacRimi
2026-10-15 19:05:19 +02:00
parent 1ca028481d
commit abbb3afc0c
2 changed files with 66 additions and 22 deletions
@@ -7,7 +7,7 @@ from pathlib import Path
import subprocess import subprocess
from types import SimpleNamespace from types import SimpleNamespace
from unittest import TestCase from unittest import TestCase
from unittest.mock import Mock from unittest.mock import Mock, patch
ROOT = Path(__file__).resolve().parents[3] ROOT = Path(__file__).resolve().parents[3]
MENU = ROOT / 'oci/src/proxmenux_oci/management.py' MENU = ROOT / 'oci/src/proxmenux_oci/management.py'
@@ -48,6 +48,42 @@ class SelectionSetupWording(TestCase):
ui.message.assert_called_once_with(EMPTY, 'OCI management') ui.message.assert_called_once_with(EMPTY, 'OCI management')
ui.choose.assert_not_called() ui.choose.assert_not_called()
def _menu(self, inventories, selections):
rows = iter(inventories)
ui = SimpleNamespace(message=Mock(), choose=Mock(side_effect=selections))
refresh = Mock(return_value={})
manage = Mock(return_value=True)
fn = extracted('_interactive_management', os=SimpleNamespace(geteuid=lambda: 0),
shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: next(rows),
_clean_orphans=lambda project: None,
recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None,
carry_records=refresh, manage_instance=manage, menu_size=lambda: 10,
STATUS_LABELS={}, translate=lambda s: s)
fn(Path('/fixture'), ui)
return ui, refresh, manage
def test_the_list_comes_back_after_an_operation_without_refreshing_every_copy(self):
row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'}
ui, refresh, manage = self._menu([[row], [row]], ['101', None])
self.assertEqual(ui.choose.call_count, 2)
manage.assert_called_once()
refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=True)
def test_removing_the_last_application_returns_without_a_message(self):
row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'}
ui, _, manage = self._menu([[row], []], ['101'])
manage.assert_called_once()
ui.message.assert_not_called()
def test_the_watchdog_refreshes_the_copy_of_the_containers_it_changed(self):
refresh = Mock(return_value={})
watchdog = SimpleNamespace(set_watchdog=Mock(side_effect=[[101, 102]]))
fn = extracted('set_watchdog', sys=SimpleNamespace(path=[]), carry_records=refresh,
instances=SimpleNamespace(ROOT=Path('/fixture')))
with patch.dict('sys.modules', {'oci_watchdog': watchdog}):
self.assertTrue(fn(Path('/fixture'), [102], True))
refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, vmids=[101, 102])
def test_selector_filters_missing_guest_without_erasing_saved_contract(self): def test_selector_filters_missing_guest_without_erasing_saved_contract(self):
class Entry: class Entry:
name = '101' name = '101'
+29 -21
View File
@@ -254,27 +254,33 @@ def _interactive_management(project, ui):
recover_automatically(project) recover_automatically(project)
offer_recovery(project, ui) offer_recovery(project, ui)
carry_records(project, mount_stopped=False, shown=True) carry_records(project, mount_stopped=False, shown=True)
rows = saved_inventory(project) # After each operation the list is shown again. Every operation leaves
if not rows: # the copies of the records up to date, so they are not all refreshed again.
ui.message(translate('No registered OCI containers are available for selection on this host.'), translate('OCI management')) first = True
return while True:
# Same layout as the catalog lists; only an unusual state is shown. rows = saved_inventory(project)
tag_width = max(len(str(r['vmid'])) for r in rows) if not rows:
header = f" {'CT':<{tag_width + 2}}{translate('Application')[:32]:<32} {translate('Image')}" if first:
options = [] ui.message(translate('No registered OCI containers are available for selection on this host.'), translate('OCI management'))
for r in rows: return
line = f"{(r['title'] or r['hostname'])[:32]:<32} " first = False
if r['status'] == 'installed': # Same layout as the catalog lists; only an unusual state is shown.
line += r['image'][:30] tag_width = max(len(str(r['vmid'])) for r in rows)
else: header = f" {'CT':<{tag_width + 2}}{translate('Application')[:32]:<32} {translate('Image')}"
line += f"\\Z1{translate(STATUS_LABELS.get(r['status'], r['status']))}\\Zn" options = []
options.append((str(r['vmid']), f"{line:<72}")) for r in rows:
selection = ui.choose(header, options, size=menu_size(), colors=True, line = f"{(r['title'] or r['hostname'])[:32]:<32} "
title=translate('Manage installed OCI applications')) if r['status'] == 'installed':
if selection is None: line += r['image'][:30]
return else:
row = next(r for r in rows if str(r['vmid']) == selection) line += f"\\Z1{translate(STATUS_LABELS.get(r['status'], r['status']))}\\Zn"
manage_instance(project, ui, row) options.append((str(r['vmid']), f"{line:<72}"))
selection = ui.choose(header, options, size=menu_size(), colors=True,
title=translate('Manage installed OCI applications'))
if selection is None:
return
row = next(r for r in rows if str(r['vmid']) == selection)
manage_instance(project, ui, row)
def set_watchdog(project, vmids, enabled): def set_watchdog(project, vmids, enabled):
@@ -291,6 +297,8 @@ def set_watchdog(project, vmids, enabled):
done.update(oci_watchdog.set_watchdog(instances.ROOT, vmid, enabled)) done.update(oci_watchdog.set_watchdog(instances.ROOT, vmid, enabled))
except (OSError, ValueError, KeyError): except (OSError, ValueError, KeyError):
return False return False
# The copy a restore on another host reads carries the watchdog as well.
carry_records(project, mount_stopped=False, vmids=sorted(done))
return True return True