[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Récupérer des fichiers dans le disque d'une VM, sans la démarrer.
|
|
|
|
|
|
|
|
|
|
Une VM qui ne démarre plus garde ses fichiers, et libguestfs sait monter son
|
|
|
|
|
qcow2 sans elle. Le risque n'est pas la lecture : c'est d'ouvrir en ÉCRITURE
|
|
|
|
|
le disque d'une machine allumée, ce qui corrompt son système de fichiers.
|
|
|
|
|
|
|
|
|
|
Ce que ces tests gardent :
|
|
|
|
|
|
|
|
|
|
- « --ro » sur CHAQUE commande, sans exception : c'est lui qui rend l'opération
|
|
|
|
|
sûre sur une VM en marche ;
|
|
|
|
|
- le nom du paquet suit la distribution — celui de Debian n'existe nulle part
|
|
|
|
|
ailleurs ;
|
|
|
|
|
- les chemins saisis par l'utilisateur sont échappés avant d'atteindre le
|
|
|
|
|
shell.
|
|
|
|
|
"""
|
|
|
|
|
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
import re
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
import shlex
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo import qemu_recover as qr # noqa: E402
|
|
|
|
|
from script.todo.todo import TODO # noqa: E402
|
|
|
|
|
|
|
|
|
|
DISQUE = "/var/lib/libvirt/images/vm-a.qcow2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LesOutils(unittest.TestCase):
|
|
|
|
|
def test_each_package_manager_gets_its_own_name(self):
|
|
|
|
|
"""« libguestfs-tools » est le nom Debian : il n'existe ni sur Arch,
|
|
|
|
|
ni sur Fedora, ni sur openSUSE."""
|
|
|
|
|
attendu = {
|
|
|
|
|
"apt-get": "libguestfs-tools",
|
|
|
|
|
"dnf": "guestfs-tools",
|
|
|
|
|
"pacman": "libguestfs",
|
|
|
|
|
"zypper": "guestfs-tools",
|
|
|
|
|
}
|
|
|
|
|
for binaire, paquet in attendu.items():
|
|
|
|
|
with self.subTest(binaire=binaire):
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
qr.shutil,
|
|
|
|
|
"which",
|
|
|
|
|
side_effect=lambda b, cible=binaire: (
|
|
|
|
|
"/usr/bin/x" if b == cible else None
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
cmd, nom = qr.guestfs_install_cmd()
|
|
|
|
|
self.assertEqual(paquet, nom)
|
|
|
|
|
self.assertIn(paquet, cmd)
|
|
|
|
|
|
|
|
|
|
def test_an_unknown_host_proposes_nothing(self):
|
|
|
|
|
"""Proposer une commande qui échouera vaut moins que de dire qu'on ne
|
|
|
|
|
sait pas — sur macOS, libguestfs ne tourne pas."""
|
|
|
|
|
with mock.patch.object(qr.shutil, "which", return_value=None):
|
|
|
|
|
self.assertEqual((None, None), qr.guestfs_install_cmd())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LaLectureEstToujoursSeule(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
|
|
|
|
def test_every_command_carries_read_only(self):
|
|
|
|
|
"""Sans « --ro », ouvrir le disque d'une VM allumée corrompt son
|
|
|
|
|
système de fichiers. Aucune commande n'a de raison de s'en passer."""
|
|
|
|
|
cmds = [
|
|
|
|
|
self.todo._qemu_guestfish_cmd(DISQUE),
|
|
|
|
|
self.todo._qemu_guestfish_cmd(DISQUE, "run", "list-filesystems"),
|
|
|
|
|
self.todo._qemu_guestfish_cmd(
|
|
|
|
|
DISQUE, "run", "mount /dev/sda3 /", "ls /home"
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
for cmd in cmds:
|
|
|
|
|
with self.subTest(cmd=cmd):
|
|
|
|
|
self.assertIn("--ro", cmd)
|
|
|
|
|
self.assertIn(shlex.quote(DISQUE), cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_commands_are_chained_the_guestfish_way(self):
|
|
|
|
|
cmd = self.todo._qemu_guestfish_cmd(
|
|
|
|
|
DISQUE, "run", "mount /dev/sda3 /", "ls /home"
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("run : mount /dev/sda3 / : ls /home", cmd)
|
|
|
|
|
|
|
|
|
|
def test_a_hostile_path_cannot_escape_the_shell(self):
|
|
|
|
|
"""Le chemin vient de l'utilisateur : sans échappement, « ; » ouvre
|
|
|
|
|
une seconde commande."""
|
|
|
|
|
cmd = self.todo._qemu_guestfish_cmd("/tmp/x.qcow2; rm -rf /", "run")
|
|
|
|
|
# Le chemin entier tient dans UNE apostrophe : le « ; » y est du
|
|
|
|
|
# texte, pas un séparateur de commande.
|
|
|
|
|
self.assertIn("'/tmp/x.qcow2; rm -rf /'", cmd)
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"qcow2; rm", cmd.replace("'/tmp/x.qcow2; rm -rf /'", "")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LaVmAllumee(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
|
|
|
|
def test_a_stopped_vm_goes_straight_through(self):
|
|
|
|
|
self.todo._qemu_domstate = lambda n: "shut off"
|
|
|
|
|
self.assertTrue(self.todo._qemu_recover_ready("vm-a"))
|
|
|
|
|
|
|
|
|
|
def test_a_running_vm_offers_three_ways(self):
|
|
|
|
|
self.todo._qemu_domstate = lambda n: "running"
|
|
|
|
|
vus = {}
|
|
|
|
|
|
|
|
|
|
def faux_pick(titre, valeurs, defaut, labels=None):
|
|
|
|
|
vus["valeurs"] = valeurs
|
|
|
|
|
vus["defaut"] = defaut
|
|
|
|
|
return "read"
|
|
|
|
|
|
|
|
|
|
self.todo._qemu_pick = faux_pick
|
|
|
|
|
with mock.patch("builtins.print"):
|
|
|
|
|
self.assertTrue(self.todo._qemu_recover_ready("vm-a"))
|
|
|
|
|
self.assertEqual(["read", "shutdown", "cancel"], vus["valeurs"])
|
|
|
|
|
# L'arrêt propre est le défaut : c'est la seule voie qui donne une
|
|
|
|
|
# copie fidèle.
|
|
|
|
|
self.assertEqual("shutdown", vus["defaut"])
|
|
|
|
|
|
|
|
|
|
def test_cancelling_stops_everything(self):
|
|
|
|
|
self.todo._qemu_domstate = lambda n: "running"
|
|
|
|
|
self.todo._qemu_pick = lambda *a, **k: "cancel"
|
|
|
|
|
with mock.patch("builtins.print"):
|
|
|
|
|
self.assertFalse(self.todo._qemu_recover_ready("vm-a"))
|
|
|
|
|
|
|
|
|
|
def test_choosing_shutdown_waits_for_it(self):
|
|
|
|
|
self.todo._qemu_domstate = lambda n: "running"
|
|
|
|
|
self.todo._qemu_pick = lambda *a, **k: "shutdown"
|
|
|
|
|
appels = []
|
|
|
|
|
self.todo._qemu_shutdown_wait = lambda n: appels.append(n) or True
|
|
|
|
|
with mock.patch("builtins.print"):
|
|
|
|
|
self.assertTrue(self.todo._qemu_recover_ready("vm-a"))
|
|
|
|
|
self.assertEqual(["vm-a"], appels)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LExtraction(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.lances = []
|
|
|
|
|
self.todo.execute = mock.MagicMock()
|
|
|
|
|
self.todo.execute.exec_command_live.side_effect = (
|
|
|
|
|
lambda cmd, **k: self.lances.append(cmd) or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_copy_out_names_both_ends(self):
|
|
|
|
|
with mock.patch.object(qr.os, "makedirs"), mock.patch(
|
|
|
|
|
"builtins.print"
|
|
|
|
|
):
|
|
|
|
|
ok = self.todo._qemu_recover_copy_out(
|
|
|
|
|
DISQUE, "/dev/sda3", "/home/erplibre", "/tmp/vm-a-backup"
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(ok)
|
|
|
|
|
cmd = self.lances[0]
|
|
|
|
|
self.assertIn("copy-out /home/erplibre /tmp/vm-a-backup", cmd)
|
|
|
|
|
self.assertIn("mount /dev/sda3 /", cmd)
|
|
|
|
|
self.assertIn("--ro", cmd)
|
|
|
|
|
|
|
|
|
|
def test_an_uncreatable_destination_stops_before_running(self):
|
|
|
|
|
"""guestfish s'arrêterait sur une erreur qui ne dit pas laquelle des
|
|
|
|
|
deux extrémités manque."""
|
|
|
|
|
with mock.patch.object(
|
|
|
|
|
qr.os, "makedirs", side_effect=OSError("lecture seule")
|
|
|
|
|
), mock.patch("builtins.print"):
|
|
|
|
|
ok = self.todo._qemu_recover_copy_out(
|
|
|
|
|
DISQUE, "/dev/sda3", "/home", "/interdit"
|
|
|
|
|
)
|
|
|
|
|
self.assertFalse(ok)
|
|
|
|
|
self.assertEqual([], self.lances)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LesDiagnostics(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.lances = []
|
|
|
|
|
self.todo.execute = mock.MagicMock()
|
|
|
|
|
self.todo.execute.exec_command_live.side_effect = (
|
|
|
|
|
lambda cmd, **k: self.lances.append(cmd) or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_four_probes_answer_four_questions(self):
|
|
|
|
|
with mock.patch("builtins.print"):
|
|
|
|
|
self.todo._qemu_recover_diagnostics(DISQUE)
|
|
|
|
|
joint = " ".join(self.lances)
|
|
|
|
|
for attendu in (
|
|
|
|
|
"virt-filesystems",
|
|
|
|
|
"virt-df",
|
|
|
|
|
"inspect-os",
|
|
|
|
|
"libguestfs-test-tool",
|
|
|
|
|
):
|
|
|
|
|
self.assertIn(attendu, joint)
|
|
|
|
|
|
|
|
|
|
def test_no_diagnostic_writes_to_the_disk(self):
|
|
|
|
|
with mock.patch("builtins.print"):
|
|
|
|
|
self.todo._qemu_recover_diagnostics(DISQUE)
|
|
|
|
|
for cmd in self.lances:
|
|
|
|
|
if "guestfish" in cmd:
|
|
|
|
|
self.assertIn("--ro", cmd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LeMenu(unittest.TestCase):
|
|
|
|
|
def test_the_entry_is_wired_in_the_manage_section(self):
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
source = Path("script/todo/qemu_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
self.assertIn("Recover files from a VM disk (libguestfs)", source)
|
|
|
|
|
self.assertIn("self._qemu_recover_files()", source)
|
|
|
|
|
|
|
|
|
|
def test_the_config_entry_still_reaches_its_command(self):
|
|
|
|
|
"""L'entrée du catalogue vient de todo.json : elle n'a pas de branche
|
|
|
|
|
« elif » et dépend du repli par indice. Insérer une entrée codée en
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
dur la décale, et un décalage manqué lancerait la mauvaise. Le numéro
|
|
|
|
|
se lit sur le menu rendu plutôt qu'écrit ici en dur."""
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
from unittest import mock as m
|
|
|
|
|
|
|
|
|
|
from script.todo.todo_i18n import set_lang
|
|
|
|
|
|
|
|
|
|
set_lang("fr")
|
|
|
|
|
todo = TODO()
|
|
|
|
|
todo._menu_header = lambda: "x"
|
|
|
|
|
lancees = []
|
|
|
|
|
todo.execute_from_configuration = lambda e: lancees.append(e)
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
# Le numéro se DÉDUIT du menu : l'entrée de config est la dernière
|
|
|
|
|
# des entrées numérotées. L'écrire en dur ferait passer le test au
|
|
|
|
|
# premier réarrangement de sections, sans rien prouver.
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
with m.patch("script.todo.qemu_menu.click") as click, m.patch.object(
|
|
|
|
|
todo, "_qemu_ensure_tools", return_value=True
|
|
|
|
|
), m.patch("builtins.print"):
|
[ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.
--- EN ---
A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.
Assisted-by: Claude Opus 5
2026-09-03 04:49:23 -04:00
|
|
|
click.prompt.side_effect = ["0"]
|
|
|
|
|
todo.prompt_execute_qemu()
|
|
|
|
|
aide = click.prompt.call_args[0][0]
|
|
|
|
|
numeros = re.findall(r"^\[(\d+)\]", aide, re.M)
|
|
|
|
|
dernier = max(int(n) for n in numeros)
|
|
|
|
|
with m.patch("script.todo.qemu_menu.click") as click, m.patch.object(
|
|
|
|
|
todo, "_qemu_ensure_tools", return_value=True
|
|
|
|
|
), m.patch("builtins.print"):
|
|
|
|
|
click.prompt.side_effect = [str(dernier), "0"]
|
[ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.
Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.
--- EN ---
A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.
Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.
Assisted-by: Claude Opus 5
2026-09-03 01:23:34 -04:00
|
|
|
todo.prompt_execute_qemu()
|
|
|
|
|
self.assertEqual(1, len(lancees), lancees)
|
|
|
|
|
self.assertIn("dry-run", lancees[0].get("bash_command", ""))
|
|
|
|
|
|
|
|
|
|
def test_the_branches_stay_in_order_and_unique(self):
|
|
|
|
|
"""Insérer une entrée décale tout ce qui suit : un numéro en double
|
|
|
|
|
rendrait une commande inatteignable."""
|
|
|
|
|
import re
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
source = Path("script/todo/qemu_menu.py").read_text(encoding="utf-8")
|
|
|
|
|
debut = source.index("def prompt_execute_qemu")
|
|
|
|
|
corps = source[debut : source.index("\n def ", debut + 10)]
|
|
|
|
|
nums = [int(n) for n in re.findall(r'status == "(\d+)"', corps)]
|
|
|
|
|
self.assertEqual(sorted(nums), nums, nums)
|
|
|
|
|
self.assertEqual(len(set(nums)), len(nums), nums)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|