erplibre/test/test_qemu_install_summary.py

205 lines
7.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Résumé d'un journal d'installation : ce qui a échoué doit se voir.
Le détail des erreurs cherchait la sous-chaîne « error ». Or le journal de
l'installation qui a réellement échoué — erplibre-ubuntu-2604-gnome, APK tué
par le noyau — ne contient AUCUNE ligne « error » : 0 sur 8765, mesuré. Le
volet annonçait donc « aucune erreur détectée » sur une installation ratée,
et le tableau de bord comptait 0 erreur.
Ces tests fixent la règle inverse : une étape en échec, un « FAILURE » de
Gradle, une trace Python ou une mort par mémoire se voient, et le résumé les
présente AVANT les centaines de lignes du détail.
"""
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "script/todo"))
import qemu_install_monitor as m # noqa: E402
# Journal réduit à sa forme réelle : les marqueurs de l'installation, puis
# l'échec tel que Gradle l'écrit. Aucune ligne ne contient « error ».
LOG_GRADLE_OOM = """\
== ERPLibre mobile, SDK Android (long) ==
-> venv ERPLibre (tout ce qui suit en dépend)
-> dépendances npm
-> APK debug (gradle)
⚠ ÉCHEC : APK debug (gradle)
aucun motif connu, dernières lignes :
FAILURE: Build failed with an exception.
* What went wrong:
Gradle build daemon disappeared unexpectedly (it may have been killed)
⚠ aucun APK produit
__ERPLIBRE_EXIT__ 1
"""
def _log(text):
fh = tempfile.NamedTemporaryFile(
"w", suffix=".log", delete=False, encoding="utf-8"
)
fh.write(text)
fh.close()
return fh.name
class TestFailedStepsAreSeen(unittest.TestCase):
def setUp(self):
self.path = _log(LOG_GRADLE_OOM)
def tearDown(self):
Path(self.path).unlink(missing_ok=True)
def test_the_reference_log_has_no_line_saying_error(self):
"""La prémisse de tout le reste : la détection par sous-chaîne ne
pouvait RIEN trouver ici."""
self.assertNotIn("error", LOG_GRADLE_OOM.lower())
def test_the_failed_step_is_named(self):
got = m.scan_log_summary(self.path)
self.assertEqual(
[s["label"] for s in got["steps"]], ["APK debug (gradle)"]
)
def test_the_step_carries_its_diagnostic(self):
"""L'échec nomme l'étape ; c'est le diagnostic qui porte la cause."""
diag = "\n".join(m.scan_log_summary(self.path)["steps"][0]["diag"])
self.assertIn("FAILURE: Build failed", diag)
self.assertIn("daemon disappeared", diag)
def test_the_exit_marker_is_not_a_diagnostic(self):
diag = "\n".join(m.scan_log_summary(self.path)["steps"][0]["diag"])
self.assertNotIn(m.EXIT_MARKER, diag)
def test_the_diagnostic_stops_at_the_next_step(self):
"""Sinon le diagnostic avale la suite de l'installation et ne désigne
plus rien."""
text = LOG_GRADLE_OOM + " -> étape suivante\n bruit\n"
path = _log(text)
try:
diag = "\n".join(m.scan_log_summary(path)["steps"][0]["diag"])
finally:
Path(path).unlink(missing_ok=True)
self.assertNotIn("bruit", diag)
def test_hard_signals_are_listed(self):
hard = " ".join(
h["text"] for h in m.scan_log_summary(self.path)["hard"]
)
self.assertIn("FAILURE", hard)
self.assertIn("disappeared unexpectedly", hard)
def test_the_dashboard_no_longer_counts_zero_errors(self):
"""Le compte alimente le tableau de bord : « 0 erreur » sur une
installation morte est un mensonge, pas une nuance."""
nerr, _ = m.scan_log_errors(self.path)
self.assertGreater(nerr, 0)
def test_the_detail_pane_is_no_longer_empty(self):
errs, _ = m.scan_log_error_lines(self.path)
self.assertTrue(errs)
self.assertTrue(any("ÉCHEC" in e or "FAILURE" in e for e in errs))
class TestOtherRealFailures(unittest.TestCase):
"""Chaque motif dur est là parce qu'il est apparu dans un vrai journal."""
def _first_hard(self, line):
path = _log(f" -> étape\n{line}\n")
try:
return m.scan_log_summary(path)["hard"]
finally:
Path(path).unlink(missing_ok=True)
def test_python_traceback(self):
self.assertTrue(self._first_hard("Traceback (most recent call last):"))
def test_git_fatal(self):
self.assertTrue(self._first_hard("fatal: repository not found"))
def test_apt_missing_package(self):
self.assertTrue(
self._first_hard("E: Unable to locate package python3.12-venv")
)
def test_kernel_oom(self):
self.assertTrue(
self._first_hard("Out of memory: Killed process 37603 (java)")
)
def test_missing_command(self):
self.assertTrue(self._first_hard("bash: emulator: command not found"))
def test_a_benign_probe_is_not_a_hard_signal(self):
"""« No such file or directory » sortait 5 fois sur 7 d'une sonde
bénigne (« cat: .odoo-version ») : le bruit dilue un résumé dont tout
l'intérêt est d'être court."""
self.assertFalse(
self._first_hard("cat: .odoo-version: No such file or directory")
)
class TestGrouping(unittest.TestCase):
def test_repeats_are_counted_not_repeated(self):
"""Un journal répète la même erreur des centaines de fois avec un
chemin qui change : on veut « ×200 », pas 200 lignes."""
lines = "\n".join(
f"ERROR: cannot read /var/lib/x/file{i}.txt" for i in range(200)
)
path = _log(lines + "\n")
try:
groups = m.scan_log_summary(path)["groups"]
finally:
Path(path).unlink(missing_ok=True)
self.assertEqual(len(groups), 1)
self.assertEqual(groups[0]["count"], 200)
def test_the_most_frequent_comes_first(self):
path = _log(
"ERROR: rare thing\n"
+ "\n".join(f"ERROR: common {i}" for i in range(5))
+ "\n"
)
try:
groups = m.scan_log_summary(path)["groups"]
finally:
Path(path).unlink(missing_ok=True)
self.assertEqual(groups[0]["count"], 5)
def test_warnings_are_grouped_apart_from_errors(self):
path = _log("WARNING: a\nERROR: b\n")
try:
kinds = {g["kind"] for g in m.scan_log_summary(path)["groups"]}
finally:
Path(path).unlink(missing_ok=True)
self.assertEqual(kinds, {"error", "warning"})
class TestQuietLogs(unittest.TestCase):
def test_a_clean_log_stays_clean(self):
"""Le résumé ne doit pas inventer d'échec là où il n'y en a pas."""
path = _log("== installation ==\n -> étape\n ✅ terminé\n")
try:
got = m.scan_log_summary(path)
finally:
Path(path).unlink(missing_ok=True)
self.assertEqual(got["steps"], [])
self.assertEqual(got["hard"], [])
self.assertEqual(got["groups"], [])
def test_a_missing_log_is_not_a_crash(self):
got = m.scan_log_summary("/nonexistent/erplibre.log")
self.assertEqual(got["steps"], [])
self.assertEqual(got["nerr"], 0)
if __name__ == "__main__":
unittest.main()