Une VM sans console peut vouloir un virtio-gpu accéléré — rendu hors écran, ou émulateur qui tourne dedans — et « auto » ne l'accorde jamais : il s'abstient sans écran, pour ne pas poser un périphérique vidéo que personne n'a demandé. La case envoie donc « --gpu on », qui l'accorde désormais. « --graphics none » est alors écarté : il dit « aucun affichage », et egl-headless EST un affichage. Le repli le rend quand la 3D tombe, sinon la VM repartirait sur le défaut de virt-install. Vérifié : 8 tests neufs et 2 sur le repli, rougis par trois mutations. Le rendu de la case dans le terminal plein écran n'est pas testé. --- EN --- A VM without a console may want an accelerated virtio-gpu — offscreen rendering, or an emulator running inside — and « auto » never grants it: it abstains without a screen, so as not to add a video device nobody asked for. The box therefore sends « --gpu on », which now grants it. « --graphics none » is then dropped: it means « no display », and egl-headless IS a display. The fallback gives it back when 3D fails, otherwise the VM would restart on virt-install's default. Checked: 8 new tests and 2 on the fallback, turned red by three mutations. The box's rendering in the full-screen terminal is not tested. Assisted-by: Claude Opus 5
210 lines
7.4 KiB
Python
210 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Un nœud de rendu présent ne prouve pas que la 3D marche.
|
|
|
|
QEMU refuse de démarrer le domaine quand EGL ne s'initialise pas sur le nœud,
|
|
et il le dit seulement à ce moment-là : « egl: eglInitialize failed:
|
|
EGL_NOT_INITIALIZED », puis « egl: render node init failed ». La détection ne
|
|
voit qu'un fichier dans /dev/dri, et aucun test préalable ne distingue un GPU
|
|
utilisable d'un nœud qui existe sans pile EGL.
|
|
|
|
Ce que ces tests gardent :
|
|
|
|
- une VM graphique naît quand même, en rendu logiciel, plutôt que d'échouer ;
|
|
- le domaine de l'essai raté est retiré avant la seconde tentative, sinon le
|
|
nom est pris et la VM reste celle qui ne démarre pas ;
|
|
- « --gpu on » est une exigence : elle n'est pas trahie en silence ;
|
|
- un échec qui n'est PAS celui d'EGL n'est pas rattrapé.
|
|
"""
|
|
|
|
import importlib.util
|
|
import io
|
|
import sys
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
sys.argv = ["todo.py"]
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _deploy_qemu():
|
|
path = RACINE / "script/qemu/deploy_qemu.py"
|
|
spec = importlib.util.spec_from_file_location("deploy_qemu", path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
DQ = _deploy_qemu()
|
|
|
|
SORTIE_EGL = (
|
|
"ERROR erreur interne : le processus s'est arrêté pendant la"
|
|
" connexion au moniteur: qemu-system-x86_64: egl: eglInitialize failed:"
|
|
" EGL_NOT_INITIALIZED\n"
|
|
"qemu-system-x86_64: egl: render node init failed\n"
|
|
)
|
|
SORTIE_AUTRE = "ERROR Unknown OS name 'ubuntu99.04'\n"
|
|
|
|
|
|
class Signature(unittest.TestCase):
|
|
def test_it_recognises_the_egl_failure(self):
|
|
self.assertTrue(DQ.egl_failed(SORTIE_EGL))
|
|
|
|
def test_it_does_not_recognise_another_failure(self):
|
|
self.assertFalse(DQ.egl_failed(SORTIE_AUTRE))
|
|
self.assertFalse(DQ.egl_failed(""))
|
|
|
|
|
|
class Repli(unittest.TestCase):
|
|
"""virt_install avec un GPU détecté, dont EGL ne démarre pas."""
|
|
|
|
def _args(self, gpu="auto"):
|
|
return SimpleNamespace(
|
|
name="vm-a",
|
|
memory=4096,
|
|
vcpus=2,
|
|
arch="amd64",
|
|
network="network=default,model=virtio",
|
|
graphics="none",
|
|
desktop=True,
|
|
gpu=gpu,
|
|
gpu_node="/dev/dri/renderD128",
|
|
attach_console=False,
|
|
bios=False,
|
|
)
|
|
|
|
def _lancer(self, gpu="auto", sortie=SORTIE_EGL, code=1):
|
|
"""Rend (commandes lancées, SystemExit ou None)."""
|
|
lances = []
|
|
|
|
class FauxRunner:
|
|
dry_run = False
|
|
use_sudo = False
|
|
|
|
def run(self, cmd, *, privileged=False, check=True, capture=False):
|
|
lances.append(list(cmd))
|
|
if capture:
|
|
return (code, sortie)
|
|
return None
|
|
|
|
with mock.patch.object(
|
|
DQ, "host_arch", return_value="amd64"
|
|
), mock.patch.object(
|
|
DQ, "kvm_available", return_value=True
|
|
), mock.patch.object(
|
|
DQ, "os"
|
|
) as faux_os:
|
|
faux_os.getuid.return_value = 1000
|
|
faux_os.makedirs.return_value = None
|
|
buf = io.StringIO()
|
|
with redirect_stdout(buf):
|
|
try:
|
|
DQ.virt_install(
|
|
self._args(gpu),
|
|
Path("/tmp/d.qcow2"),
|
|
Path("/tmp/s.iso"),
|
|
"ubuntu26.04",
|
|
FauxRunner(),
|
|
)
|
|
sortie_exc = None
|
|
except SystemExit as exc:
|
|
sortie_exc = exc
|
|
return lances, sortie_exc, buf.getvalue()
|
|
|
|
def test_the_first_attempt_carries_the_3d(self):
|
|
lances, _, _ = self._lancer()
|
|
self.assertIn("model.acceleration.accel3d=on", " ".join(lances[0]))
|
|
|
|
def test_a_second_attempt_runs_without_the_3d(self):
|
|
lances, exc, rendu = self._lancer()
|
|
self.assertIsNone(exc, rendu)
|
|
dernier = " ".join(lances[-1])
|
|
self.assertNotIn("accel3d", dernier)
|
|
self.assertNotIn("egl-headless", dernier)
|
|
# L'écran doit revenir : une VM graphique sans --video n'en a plus.
|
|
self.assertIn("--video virtio", dernier)
|
|
|
|
def test_the_failed_domain_is_undefined_first(self):
|
|
"""Sans ce retrait, virt-install refuse le nom déjà pris et la VM
|
|
reste celle qui ne démarre pas."""
|
|
lances, _, _ = self._lancer()
|
|
milieu = " ".join(lances[1])
|
|
self.assertIn("undefine", milieu)
|
|
self.assertIn("vm-a", milieu)
|
|
self.assertLess(1, len(lances) - 1)
|
|
|
|
def test_gpu_on_also_falls_back_but_says_so(self):
|
|
"""Une VM qu'on n'a pas est pire qu'une VM sans 3D. Le repli vaut donc
|
|
aussi pour une 3D demandée — à condition de nommer ce qui manque, sans
|
|
quoi l'utilisateur croirait avoir obtenu ce qu'il a coché."""
|
|
lances, exc, rendu = self._lancer(gpu="on")
|
|
self.assertIsNone(exc, rendu)
|
|
self.assertIn("DEMANDÉE", rendu)
|
|
self.assertNotIn("accel3d", " ".join(lances[-1]))
|
|
|
|
def test_another_failure_is_not_retried(self):
|
|
lances, exc, _ = self._lancer(sortie=SORTIE_AUTRE)
|
|
self.assertIsInstance(exc, SystemExit)
|
|
self.assertEqual(1, len(lances))
|
|
|
|
def _sans_ecran(self, resultat):
|
|
"""virt_install sur une VM sans console dont la 3D est demandée.
|
|
|
|
`resultat` est ce que rend le runner en mode capture. Rend les
|
|
commandes lancées."""
|
|
args = self._args(gpu="on")
|
|
args.desktop = False
|
|
lances = []
|
|
|
|
class FauxRunner:
|
|
dry_run = False
|
|
use_sudo = False
|
|
|
|
def run(self, cmd, *, privileged=False, check=True, capture=False):
|
|
lances.append(list(cmd))
|
|
return resultat if capture else None
|
|
|
|
with mock.patch.object(
|
|
DQ, "host_arch", return_value="amd64"
|
|
), mock.patch.object(
|
|
DQ, "kvm_available", return_value=True
|
|
), mock.patch.object(
|
|
DQ, "os"
|
|
) as faux_os:
|
|
faux_os.getuid.return_value = 1000
|
|
with redirect_stdout(io.StringIO()):
|
|
DQ.virt_install(
|
|
args,
|
|
Path("/tmp/d.qcow2"),
|
|
Path("/tmp/s.iso"),
|
|
"ubuntu26.04",
|
|
FauxRunner(),
|
|
)
|
|
return lances
|
|
|
|
def test_a_screenless_3d_vm_keeps_egl_as_its_only_display(self):
|
|
"""« --graphics none » dit « aucun affichage » : le poser à côté d'un
|
|
egl-headless, qui EST un affichage, se contredit."""
|
|
rendu = " ".join(self._sans_ecran((0, ""))[0])
|
|
self.assertIn("egl-headless", rendu)
|
|
self.assertNotIn("--graphics none", rendu)
|
|
|
|
def test_dropping_the_3d_gives_the_display_back(self):
|
|
"""Le repli doit rendre le « --graphics none » écarté pour la 3D,
|
|
sinon la VM repart sur le défaut de virt-install."""
|
|
dernier = " ".join(self._sans_ecran((1, SORTIE_EGL))[-1])
|
|
self.assertIn("--graphics none", dernier)
|
|
self.assertNotIn("egl-headless", dernier)
|
|
|
|
def test_a_success_never_retries(self):
|
|
lances, exc, _ = self._lancer(sortie="", code=0)
|
|
self.assertIsNone(exc)
|
|
self.assertEqual(1, len(lances))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|