mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
feat(post-install): rebuild Log2RAM with the journal and task logs kept on disk
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user