feat(post-install): rebuild Log2RAM with the journal and task logs kept on disk

This commit is contained in:
MacRimi
2026-10-27 19:48:26 +01:00
parent ac9b8c2705
commit 4cefc0484b
57 changed files with 992 additions and 1065 deletions
+126
View File
@@ -0,0 +1,126 @@
"""Exercise only the update confirmation widget in a disposable terminal.
No update script is sourced or executed: the isolated UI command receives
synthetic package names. Requires dialog; skips on systems without it.
"""
import errno
import fcntl
import os
from pathlib import Path
import pty
import re
import select
import shutil
import signal
import struct
import termios
import time
import unittest
ROOT = Path(__file__).resolve().parents[1]
UPDATERS = (
"update-pve8.sh",
"update-pve9_2.sh",
"update-pve-safe.sh",
)
def confirmation_command(name):
source = (ROOT / "scripts/global" / name).read_text()
match = re.search(
r'^\s*if (?:! )?((?:dialog|whiptail) --(?:keep-tite --)?title '
r'"\$\(translate "Proxmox Update"\)"[^;]+); then',
source,
re.MULTILINE,
)
if not match:
raise AssertionError("Update confirmation not found: " + name)
return match.group(1)
def run_widget(command, text, keys):
child, terminal = pty.fork()
if child == 0:
fcntl.ioctl(1, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 100, 0, 0))
environment = dict(os.environ, TERM="xterm-256color", LC_ALL="C",
DIALOGRC="/dev/null", UPDATE_CONFIRMATION_TEXT=text)
# Exit-code overrides must not leak into this isolated test.
for key in list(environment):
if key.startswith("DIALOG_"):
environment.pop(key)
script = ('translate() { printf "%s" "$1"; }\n'
'menu_text=$UPDATE_CONFIRMATION_TEXT\n' + command)
os.execve("/bin/bash", ["bash", "-c", script], environment)
output = bytearray()
sent = False
status = None
deadline = time.monotonic() + 8
try:
while time.monotonic() < deadline:
if select.select([terminal], [], [], 0.1)[0]:
try:
chunk = os.read(terminal, 65536)
except OSError as error:
if error.errno != errno.EIO:
raise
chunk = b""
output.extend(chunk)
# The widget is ready once both buttons have been drawn.
plain = re.sub(rb"\x1b(?:\[[0-?]*[ -/]*[@-~]|[()][A-Za-z0-9]|.)", b"", output)
if not sent and b"Yes" in plain and b"No" in plain:
os.write(terminal, keys)
sent = True
finished, result = os.waitpid(child, os.WNOHANG)
if finished:
status = os.waitstatus_to_exitcode(result)
break
if status is None:
raise AssertionError("Confirmation did not respond to keyboard input: " + repr(bytes(output[-2000:])))
return status, bytes(output)
finally:
if status is None:
try:
os.kill(child, signal.SIGKILL)
except ProcessLookupError:
pass
os.waitpid(child, 0)
os.close(terminal)
@unittest.skipUnless(shutil.which("dialog"), "dialog is not installed")
class UpdateConfirmationTest(unittest.TestCase):
def test_enter_confirms_yes_without_tab_for_short_and_long_lists(self):
for updater in UPDATERS:
for count in (1, 80):
with self.subTest(updater=updater, packages=count):
text = "System Update Information\\n\\n" + "\\n".join(
" package-{:02d} 1.0 -> 2.0".format(i)
for i in range(count)
) + "\\n\\nDo you want to proceed with the system update?"
status, output = run_widget(confirmation_command(updater), text, b"\r")
self.assertEqual(status, 0, output)
def test_tab_can_select_no_and_escape_can_cancel(self):
for updater in UPDATERS:
for keys, expected in ((b"\t\r", 1), (b"\x1b", 255)):
with self.subTest(updater=updater, keys=keys):
status, output = run_widget(confirmation_command(updater), "Confirm update?", keys)
self.assertEqual(status, expected, output)
def test_full_list_remains_scrollable_with_yes_focused(self):
text = "\\n".join("package-{:03d}".format(i) for i in range(80))
for updater in UPDATERS:
with self.subTest(updater=updater):
# G scrolls to the end of the yes/no text without moving focus.
status, output = run_widget(confirmation_command(updater), text, b"G\r")
self.assertEqual(status, 0, output)
# Curses may repaint only changed digits rather than emit a
# complete final line. Its 100% indicator confirms the end.
self.assertIn(b"100%", output)
if __name__ == "__main__":
unittest.main()