[ADD] run : précharger le registre Odoo au lancement, sans nuire
Odoo ne charge le registre d'une base qu'à la PREMIÈRE requête : sur une base migrée, la personne qui ouvre la page attend des dizaines de secondes. La sonde prend ce temps à sa place et s'arrête à la première réponse : un 303, un 404 ou un 500 prouvent tous que le registre est chargé. Elle ne peut pas nuire : elle rend TOUJOURS 0, meurt avec run.sh par un trap et abandonne après deux minutes. Le port vient de la ligne de commande, puis de config.conf, puis du journal, exact même quand le port demandé était pris ; l'écoute sur une interface vide ou générale est sondée par le bouclage. `--erplibre-disable-warmup-http` la coupe, et elle seule est RETIRÉE avant odoo_bin.sh, qu'Odoo refuse ; `--no-http` et `--stop-after-init` aussi. --- EN --- Odoo only loads a database's registry on the FIRST request: on a migrated database, whoever opens the page waits tens of seconds. The probe takes that time instead and stops at the first answer: a 303, a 404 or a 500 all prove the registry is loaded. It cannot get in the way: it ALWAYS returns 0, dies with run.sh through a trap and gives up after two minutes. The port comes from the command line, then config.conf, then the log, exact even when the requested port was taken; an empty or wildcard listen interface is probed on the loopback. `--erplibre-disable-warmup-http` turns it off, and it alone is STRIPPED before odoo_bin.sh, which Odoo rejects; `--no-http` and `--stop-after-init` too. Assisted-by: Claude Opus 5 (cherry picked from commit d0943782029ac9a23830d8a8041fccbcac98b67d)
This commit is contained in:
parent
2437a9b5ea
commit
46eebf0859
3 changed files with 587 additions and 0 deletions
29
run.sh
29
run.sh
|
|
@ -17,6 +17,7 @@ fi
|
|||
# main : Odoo ne les connaît pas et mourrait sur « no such option ».
|
||||
# --auto-erplibre arme le choix de la base à démarrer
|
||||
# --no-cli-erplibre interdit le menu, sans interdire le choix
|
||||
# --erplibre-disable-warmup-http n'envoie pas la requête de réveil
|
||||
# Sans AUCUN argument, le choix s'arme de lui-même : c'est « make run »,
|
||||
# quelqu'un devant son terminal. Ce défaut-là reste timide — il exige un
|
||||
# terminal des deux côtés — parce que systemd lance lui aussi run.sh sans
|
||||
|
|
@ -25,6 +26,7 @@ EL_ARGS=()
|
|||
EL_AUTO=0
|
||||
EL_AUTO_EXPLICITE=0
|
||||
EL_NO_CLI=0
|
||||
EL_WARMUP=1
|
||||
[ $# -eq 0 ] && EL_AUTO=1
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
|
|
@ -33,6 +35,13 @@ while [ $# -gt 0 ]; do
|
|||
EL_AUTO_EXPLICITE=1
|
||||
;;
|
||||
--no-cli-erplibre) EL_NO_CLI=1 ;;
|
||||
--erplibre-disable-warmup-http) EL_WARMUP=0 ;;
|
||||
# Rien à réveiller si personne n'écoute. On les laisse passer à Odoo,
|
||||
# on se contente de ne pas sonder.
|
||||
--no-http|--stop-after-init)
|
||||
EL_WARMUP=0
|
||||
EL_ARGS+=("$1")
|
||||
;;
|
||||
# Tout le reste passe tel quel, y compris les arguments vides et ceux
|
||||
# qui portent des espaces : un tableau, jamais une chaîne reconstruite.
|
||||
*) EL_ARGS+=("$1") ;;
|
||||
|
|
@ -58,6 +67,26 @@ if [ "${EL_AUTO}" = "1" ] && [ -f "${EL_LIB}" ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Le réveil, EN PARALLÈLE. Odoo ne charge le registre d'une base qu'à la
|
||||
# première requête qui la concerne ; sur une base migrée cela prend des
|
||||
# dizaines de secondes, et c'est la personne qui ouvre la page qui les
|
||||
# attend. La sonde prend ce temps à sa place, se tait, et meurt avec nous.
|
||||
EL_WARMUP_PID=""
|
||||
EL_WARMUP_BIN="./script/odoo/warmup_http.py"
|
||||
if [ "${EL_WARMUP}" = "1" ] && [ "$ODOO_MODE_TEST" != "true" ] \
|
||||
&& [ -f "${EL_WARMUP_BIN}" ]; then
|
||||
EL_WARMUP_DB=()
|
||||
[ -n "${EL_DB_NAME}" ] && EL_WARMUP_DB=(-d "${EL_DB_NAME}")
|
||||
# `--` sépare NOS options de celles d'Odoo : sans lui, un « -c » destiné
|
||||
# à Odoo serait lu comme le nôtre.
|
||||
python3 "${EL_WARMUP_BIN}" -c "${CONFIG_PATH}" "${EL_WARMUP_DB[@]}" \
|
||||
-- "${EL_ARGS[@]}" 2>/dev/null &
|
||||
EL_WARMUP_PID=$!
|
||||
# Elle ne doit pas survivre au serveur qu'elle réveille. Un Ctrl-C sur
|
||||
# run.sh doit tout emporter.
|
||||
trap '[ -n "${EL_WARMUP_PID}" ] && kill "${EL_WARMUP_PID}" 2>/dev/null' EXIT
|
||||
fi
|
||||
|
||||
if [ "$ODOO_MODE_TEST" = "true" ]; then
|
||||
./odoo_bin.sh -c "${CONFIG_PATH}" --limit-time-real 99999 --limit-time-cpu 99999 --limit-memory-hard=0 --log-level=test --test-enable --no-http --stop-after-init "${EL_DB[@]}" "${EL_ARGS[@]}"
|
||||
else
|
||||
|
|
|
|||
227
script/odoo/warmup_http.py
Executable file
227
script/odoo/warmup_http.py
Executable file
|
|
@ -0,0 +1,227 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Réveiller l'instance dès qu'elle écoute, puis disparaître.
|
||||
|
||||
Odoo ne charge le registre d'une base qu'à la PREMIÈRE requête qui la
|
||||
concerne. Sur une base migrée, ce premier chargement prend des dizaines de
|
||||
secondes — et c'est la personne qui ouvre la page qui les attend. Lancée
|
||||
en parallèle de `run.sh`, cette sonde prend ce temps à sa place.
|
||||
|
||||
Trois règles, et elles tiennent au fait qu'elle tourne À CÔTÉ du serveur
|
||||
--------------------------------------------------------------------
|
||||
1. Elle se TAIT. Une seule ligne, au démarrage, pour dire quelle adresse
|
||||
elle a choisie — c'est ce qui permet de comprendre après coup un
|
||||
réveil qui n'a pas eu lieu. Ensuite plus rien : ni échec, ni trace.
|
||||
Un serveur qu'on arrête au bout de dix secondes est un cas ordinaire,
|
||||
pas une erreur, et un message d'erreur dans le journal de `run.sh`
|
||||
ferait chercher une panne qui n'existe pas.
|
||||
2. Elle s'arrête à la PREMIÈRE réponse, quelle qu'elle soit. Un 303 vers
|
||||
/web/login, un 404, un 500 : tous prouvent que le registre est chargé,
|
||||
ce qui est l'objet. Seul le refus de connexion ne compte pas.
|
||||
3. Elle meurt au bout de deux minutes. Le réveil est un confort ; une
|
||||
sonde qui survit à son serveur en est un autre, mauvais.
|
||||
|
||||
Où elle trouve le port
|
||||
----------------------
|
||||
D'abord la ligne de commande — `-p 8090` l'emporte sur tout le reste,
|
||||
c'est ce que la personne vient de taper. Puis `config.conf`, que `run.sh`
|
||||
a déjà résolu et qu'elle reçoit tel quel. Le journal d'exécution ne sert
|
||||
qu'en dernier recours : Odoo y écrit « HTTP service (werkzeug) running on
|
||||
127.0.0.1:8069 » une fois qu'il écoute, ce qui arrive parfois APRÈS le
|
||||
premier essai — mais c'est aussi la seule source exacte quand le port
|
||||
demandé était déjà pris.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.append(
|
||||
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
)
|
||||
|
||||
from script.analyse import lib_analyse # noqa: E402
|
||||
|
||||
DELAI_TOTAL = 120.0
|
||||
DELAI_ENTRE_ESSAIS = 1.0
|
||||
DELAI_REQUETE = 10.0
|
||||
PORT_PAR_DEFAUT = 8069
|
||||
HOTE_PAR_DEFAUT = "127.0.0.1"
|
||||
|
||||
# « HTTP service (werkzeug) running on 127.0.0.1:8069 »
|
||||
MOTIF_JOURNAL = re.compile(
|
||||
r"HTTP service \(werkzeug\) running on ([^:\s]+):(\d+)"
|
||||
)
|
||||
|
||||
|
||||
def port_de_la_ligne(argv):
|
||||
"""Le port demandé sur la ligne de commande, ou None.
|
||||
|
||||
`-p 8090`, `--http-port 8090` et `--http-port=8090` : les trois
|
||||
formes existent, et Odoo les accepte toutes.
|
||||
"""
|
||||
for index, argument in enumerate(argv):
|
||||
if argument in ("-p", "--http-port") and index + 1 < len(argv):
|
||||
valeur = argv[index + 1]
|
||||
if valeur.isdigit():
|
||||
return int(valeur)
|
||||
if argument.startswith("--http-port="):
|
||||
valeur = argument.split("=", 1)[1]
|
||||
if valeur.isdigit():
|
||||
return int(valeur)
|
||||
return None
|
||||
|
||||
|
||||
def adresse_du_journal(chemin, taille=8192):
|
||||
"""(hôte, port) lus dans la fin du journal, ou (None, None).
|
||||
|
||||
On ne lit que la queue : un journal de migration pèse des mégaoctets,
|
||||
et la ligne qu'on cherche est la dernière écrite.
|
||||
"""
|
||||
if not chemin or not os.path.isfile(chemin):
|
||||
return None, None
|
||||
try:
|
||||
with open(chemin, "rb") as handle:
|
||||
handle.seek(0, os.SEEK_END)
|
||||
debut = max(0, handle.tell() - taille)
|
||||
handle.seek(debut)
|
||||
queue = handle.read().decode("utf-8", "replace")
|
||||
except OSError:
|
||||
return None, None
|
||||
trouve = MOTIF_JOURNAL.findall(queue)
|
||||
if not trouve:
|
||||
return None, None
|
||||
hote, port = trouve[-1]
|
||||
return hote, int(port)
|
||||
|
||||
|
||||
def adresse(argv=(), config_path=None, journal=None):
|
||||
"""(hôte, port) à réveiller, dans l'ordre de ce qui fait autorité."""
|
||||
config = lib_analyse.read_config(config_path)
|
||||
|
||||
port = port_de_la_ligne(argv)
|
||||
hote = None
|
||||
if port is None:
|
||||
hote, port = adresse_du_journal(journal)
|
||||
if port is None:
|
||||
brut = str(config.get("http_port", "")).strip()
|
||||
port = int(brut) if brut.isdigit() else PORT_PAR_DEFAUT
|
||||
|
||||
if not hote:
|
||||
interface = str(config.get("http_interface", "")).strip()
|
||||
# Une interface vide veut dire « toutes » : on se parle à soi-même.
|
||||
# 0.0.0.0 n'est pas une adresse de destination.
|
||||
if interface.lower() in ("", "false", "none", "0.0.0.0", "::"):
|
||||
hote = HOTE_PAR_DEFAUT
|
||||
else:
|
||||
hote = interface
|
||||
return hote, port
|
||||
|
||||
|
||||
def base_a_reveiller(argv=(), config_path=None):
|
||||
"""La base dont on veut le registre, ou None.
|
||||
|
||||
`-d` d'abord : c'est celle que `run.sh` vient de choisir. Sinon
|
||||
`db_name` de la configuration, qu'Odoo utiliserait de toute façon.
|
||||
"""
|
||||
for index, argument in enumerate(argv):
|
||||
if argument in ("-d", "--database") and index + 1 < len(argv):
|
||||
return argv[index + 1]
|
||||
if argument.startswith("--database="):
|
||||
return argument.split("=", 1)[1]
|
||||
valeur = str(
|
||||
lib_analyse.read_config(config_path).get("db_name", "")
|
||||
).strip()
|
||||
return valeur if valeur.lower() not in ("", "false", "none") else None
|
||||
|
||||
|
||||
def url(hote, port, database=None):
|
||||
"""L'adresse à demander.
|
||||
|
||||
`/web/login` plutôt que `/` : elle ne redirige pas vers un tableau de
|
||||
bord, ne crée pas de session, et suffit à faire charger le registre.
|
||||
Le paramètre `db` dit LAQUELLE quand plusieurs bases répondent.
|
||||
"""
|
||||
base = f"http://{hote}:{port}/web/login"
|
||||
if database:
|
||||
from urllib.parse import quote
|
||||
|
||||
return f"{base}?db={quote(database)}"
|
||||
return base
|
||||
|
||||
|
||||
def sonder(
|
||||
cible,
|
||||
delai_total=DELAI_TOTAL,
|
||||
entre_essais=DELAI_ENTRE_ESSAIS,
|
||||
requete=DELAI_REQUETE,
|
||||
horloge=time.monotonic,
|
||||
dormir=time.sleep,
|
||||
):
|
||||
"""Demander jusqu'à obtenir une réponse. Rendre True si elle est venue.
|
||||
|
||||
N'importe quel code HTTP est une réussite : 303, 404 ou 500 prouvent
|
||||
tous que le serveur a répondu, donc que le registre est chargé. Seul
|
||||
le refus de connexion fait recommencer.
|
||||
"""
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
fin = horloge() + delai_total
|
||||
while horloge() < fin:
|
||||
try:
|
||||
with urllib.request.urlopen(cible, timeout=requete):
|
||||
return True
|
||||
except urllib.error.HTTPError:
|
||||
# Le serveur a répondu, et c'est tout ce qu'on demandait.
|
||||
return True
|
||||
except Exception: # noqa: BLE001 - un serveur pas encore prêt
|
||||
pass
|
||||
dormir(entre_essais)
|
||||
return False
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""Toujours rendre 0 : personne ne doit lire ce code de retour.
|
||||
|
||||
La sonde tourne en arrière-plan d'un `run.sh` qui, lui, rapporte
|
||||
l'état d'Odoo. Un code non nul finirait dans un journal et ferait
|
||||
croire à une panne du serveur.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Warm up the HTTP registry.")
|
||||
parser.add_argument("-c", "--config")
|
||||
parser.add_argument("-d", "--database")
|
||||
parser.add_argument("--log")
|
||||
parser.add_argument("--timeout", type=float, default=DELAI_TOTAL)
|
||||
parser.add_argument("--quiet", action="store_true")
|
||||
parser.add_argument("odoo_args", nargs="*")
|
||||
# `parse_known_args` et non `parse_args` : les arguments d'Odoo nous
|
||||
# traversent, et l'un d'eux ressemblera un jour à une option qu'on ne
|
||||
# connaît pas. `parse_args` sortirait alors par SystemExit — ce qui
|
||||
# romprait la seule promesse qui compte ici : ne jamais gêner.
|
||||
args, restes = parser.parse_known_args(argv)
|
||||
args.odoo_args = list(args.odoo_args) + list(restes)
|
||||
|
||||
try:
|
||||
hote, port = adresse(args.odoo_args, args.config, args.log)
|
||||
database = args.database or base_a_reveiller(
|
||||
args.odoo_args, args.config
|
||||
)
|
||||
cible = url(hote, port, database)
|
||||
if not args.quiet:
|
||||
print(f"🔥 warmup: {cible}", flush=True)
|
||||
sonder(cible, delai_total=args.timeout)
|
||||
except Exception: # noqa: BLE001 - le réveil est un confort, pas un dû
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
331
test/test_warmup_http.py
Normal file
331
test/test_warmup_http.py
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Le réveil HTTP : il doit se taire, et ne jamais gêner.
|
||||
|
||||
Cette sonde tourne à côté du serveur, pas devant lui. Tout ce qu'elle
|
||||
peut faire de mal, elle le ferait en parlant : un message d'erreur dans
|
||||
le journal de `run.sh` ferait chercher une panne d'Odoo qui n'existe pas,
|
||||
et un code de retour non nul ferait échouer un démarrage réussi.
|
||||
|
||||
D'où les deux propriétés que ce fichier épingle avant les autres : elle
|
||||
rend TOUJOURS 0, et elle n'écrit qu'une ligne — l'adresse choisie, parce
|
||||
que c'est la seule chose qui permette de comprendre après coup un réveil
|
||||
qui n'a pas eu lieu.
|
||||
|
||||
Le troisième sujet est l'option elle-même. `--erplibre-disable-warmup-http`
|
||||
doit être RETIRÉE avant `odoo_bin.sh` : Odoo meurt sur « no such option »,
|
||||
et le démarrage entier tombe pour un confort.
|
||||
"""
|
||||
|
||||
import io
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from script.odoo import warmup_http as warmup
|
||||
|
||||
RUN_SH = Path(__file__).resolve().parent.parent / "run.sh"
|
||||
|
||||
|
||||
class TestWhereItKnocks(unittest.TestCase):
|
||||
"""Le port, dans l'ordre de ce qui fait autorité."""
|
||||
|
||||
def test_the_command_line_wins_over_everything(self):
|
||||
for argv in (
|
||||
["-p", "8090"],
|
||||
["--http-port", "8090"],
|
||||
["--http-port=8090"],
|
||||
):
|
||||
self.assertEqual(warmup.port_de_la_ligne(argv), 8090, argv)
|
||||
|
||||
def test_a_port_that_is_not_a_number_is_ignored(self):
|
||||
self.assertIsNone(warmup.port_de_la_ligne(["-p", "huit-mille"]))
|
||||
self.assertIsNone(warmup.port_de_la_ligne(["-p"]))
|
||||
|
||||
def test_without_anything_it_falls_back_on_the_default(self):
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
warmup.lib_analyse.read_config = lambda *a, **k: {}
|
||||
try:
|
||||
self.assertEqual(
|
||||
warmup.adresse(),
|
||||
(warmup.HOTE_PAR_DEFAUT, warmup.PORT_PAR_DEFAUT),
|
||||
)
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
def test_an_empty_interface_means_ourselves(self):
|
||||
"""`http_interface =` veut dire « toutes » ; 0.0.0.0 n'est pas une
|
||||
adresse de destination."""
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
for valeur in ("", "False", "0.0.0.0", "::"):
|
||||
warmup.lib_analyse.read_config = lambda *a, v=valeur, **k: {
|
||||
"http_interface": v
|
||||
}
|
||||
try:
|
||||
self.assertEqual(warmup.adresse()[0], "127.0.0.1", valeur)
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
def test_a_real_interface_is_used_as_is(self):
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
warmup.lib_analyse.read_config = lambda *a, **k: {
|
||||
"http_interface": "10.0.0.5",
|
||||
"http_port": "8071",
|
||||
}
|
||||
try:
|
||||
self.assertEqual(warmup.adresse(), ("10.0.0.5", 8071))
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
|
||||
class TestReadingTheExecutionLog(unittest.TestCase):
|
||||
"""La seule source exacte quand le port demandé était déjà pris."""
|
||||
|
||||
def _journal(self, contenu):
|
||||
import tempfile
|
||||
|
||||
handle = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".log", delete=False, encoding="utf-8"
|
||||
)
|
||||
handle.write(contenu)
|
||||
handle.close()
|
||||
self.addCleanup(lambda: Path(handle.name).unlink(missing_ok=True))
|
||||
return handle.name
|
||||
|
||||
def test_it_reads_the_address_odoo_announced(self):
|
||||
chemin = self._journal(
|
||||
"blah\nHTTP service (werkzeug) running on 127.0.0.1:8075\nblah\n"
|
||||
)
|
||||
self.assertEqual(
|
||||
warmup.adresse_du_journal(chemin), ("127.0.0.1", 8075)
|
||||
)
|
||||
|
||||
def test_the_last_line_wins(self):
|
||||
"""Un redémarrage laisse les deux ; la dernière est la vraie."""
|
||||
chemin = self._journal(
|
||||
"HTTP service (werkzeug) running on 127.0.0.1:8069\n"
|
||||
"HTTP service (werkzeug) running on 127.0.0.1:8070\n"
|
||||
)
|
||||
self.assertEqual(warmup.adresse_du_journal(chemin)[1], 8070)
|
||||
|
||||
def test_a_missing_or_silent_log_says_nothing(self):
|
||||
self.assertEqual(warmup.adresse_du_journal(None), (None, None))
|
||||
self.assertEqual(warmup.adresse_du_journal("/pas/la"), (None, None))
|
||||
self.assertEqual(
|
||||
warmup.adresse_du_journal(self._journal("rien\n")), (None, None)
|
||||
)
|
||||
|
||||
def test_the_command_line_still_wins_over_the_log(self):
|
||||
chemin = self._journal(
|
||||
"HTTP service (werkzeug) running on 127.0.0.1:8075\n"
|
||||
)
|
||||
self.assertEqual(
|
||||
warmup.adresse(["-p", "8090"], journal=chemin)[1], 8090
|
||||
)
|
||||
|
||||
|
||||
class TestWhichDatabase(unittest.TestCase):
|
||||
def test_the_one_run_sh_just_chose(self):
|
||||
self.assertEqual(warmup.base_a_reveiller(["-d", "ma_base"]), "ma_base")
|
||||
self.assertEqual(
|
||||
warmup.base_a_reveiller(["--database=ma_base"]), "ma_base"
|
||||
)
|
||||
|
||||
def test_otherwise_the_one_odoo_would_use(self):
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
warmup.lib_analyse.read_config = lambda *a, **k: {"db_name": "prod"}
|
||||
try:
|
||||
self.assertEqual(warmup.base_a_reveiller([]), "prod")
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
def test_db_name_false_is_not_a_database(self):
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
for valeur in ("False", "", "None"):
|
||||
warmup.lib_analyse.read_config = lambda *a, v=valeur, **k: {
|
||||
"db_name": v
|
||||
}
|
||||
try:
|
||||
self.assertIsNone(warmup.base_a_reveiller([]), valeur)
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
def test_the_name_is_escaped_in_the_url(self):
|
||||
self.assertIn("db=ma%20base", warmup.url("127.0.0.1", 8069, "ma base"))
|
||||
|
||||
def test_without_a_database_the_url_carries_no_parameter(self):
|
||||
self.assertEqual(
|
||||
warmup.url("127.0.0.1", 8069), "http://127.0.0.1:8069/web/login"
|
||||
)
|
||||
|
||||
|
||||
class TestWhenItStops(unittest.TestCase):
|
||||
"""N'importe quelle réponse suffit ; seul le refus fait recommencer."""
|
||||
|
||||
def _sonder(self, reponses, delai=10.0):
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
essais = []
|
||||
temps = [0.0]
|
||||
|
||||
def faux_urlopen(cible, timeout=None):
|
||||
essais.append(cible)
|
||||
resultat = (
|
||||
reponses.pop(0) if reponses else ConnectionRefusedError()
|
||||
)
|
||||
if isinstance(resultat, Exception):
|
||||
raise resultat
|
||||
return io.BytesIO(b"")
|
||||
|
||||
vrai = urllib.request.urlopen
|
||||
urllib.request.urlopen = faux_urlopen
|
||||
try:
|
||||
venu = warmup.sonder(
|
||||
"http://x",
|
||||
delai_total=delai,
|
||||
entre_essais=0,
|
||||
horloge=lambda: temps[0],
|
||||
dormir=lambda s: temps.__setitem__(0, temps[0] + 1),
|
||||
)
|
||||
finally:
|
||||
urllib.request.urlopen = vrai
|
||||
return venu, essais
|
||||
|
||||
def test_a_first_answer_ends_it(self):
|
||||
venu, essais = self._sonder([None])
|
||||
self.assertTrue(venu)
|
||||
self.assertEqual(len(essais), 1)
|
||||
|
||||
def test_an_http_error_is_an_answer(self):
|
||||
"""303, 404, 500 : tous prouvent que le registre est chargé."""
|
||||
import urllib.error
|
||||
|
||||
erreur = urllib.error.HTTPError("u", 500, "boom", {}, None)
|
||||
venu, essais = self._sonder([erreur])
|
||||
self.assertTrue(venu)
|
||||
self.assertEqual(len(essais), 1)
|
||||
|
||||
def test_a_refused_connection_makes_it_try_again(self):
|
||||
venu, essais = self._sonder([ConnectionRefusedError(), None])
|
||||
self.assertTrue(venu)
|
||||
self.assertEqual(len(essais), 2)
|
||||
|
||||
def test_it_gives_up_at_the_deadline(self):
|
||||
venu, essais = self._sonder([], delai=5.0)
|
||||
self.assertFalse(venu)
|
||||
self.assertEqual(len(essais), 5)
|
||||
|
||||
def test_the_default_deadline_is_two_minutes(self):
|
||||
self.assertEqual(warmup.DELAI_TOTAL, 120.0)
|
||||
|
||||
|
||||
class TestItNeverGetsInTheWay(unittest.TestCase):
|
||||
"""La propriété qui compte le plus : elle ne peut pas nuire."""
|
||||
|
||||
def test_it_always_returns_zero(self):
|
||||
vrai = warmup.sonder
|
||||
warmup.sonder = lambda *a, **k: (_ for _ in ()).throw(
|
||||
RuntimeError("x")
|
||||
)
|
||||
try:
|
||||
self.assertEqual(warmup.main(["--quiet"]), 0)
|
||||
finally:
|
||||
warmup.sonder = vrai
|
||||
|
||||
def test_a_broken_config_does_not_stop_it(self):
|
||||
vrai = warmup.lib_analyse.read_config
|
||||
warmup.lib_analyse.read_config = lambda *a, **k: (_ for _ in ()).throw(
|
||||
OSError("illisible")
|
||||
)
|
||||
try:
|
||||
self.assertEqual(warmup.main(["--quiet"]), 0)
|
||||
finally:
|
||||
warmup.lib_analyse.read_config = vrai
|
||||
|
||||
def test_it_announces_the_address_it_chose(self):
|
||||
"""La seule ligne qu'elle écrit, et celle qui rend le silence
|
||||
compréhensible plus tard."""
|
||||
import contextlib
|
||||
|
||||
vrai = warmup.sonder
|
||||
warmup.sonder = lambda *a, **k: True
|
||||
sortie = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stdout(sortie):
|
||||
warmup.main(["-d", "ma_base", "--", "-p", "8090"])
|
||||
finally:
|
||||
warmup.sonder = vrai
|
||||
texte = sortie.getvalue()
|
||||
self.assertEqual(len(texte.strip().splitlines()), 1)
|
||||
self.assertIn("ma_base", texte)
|
||||
|
||||
def test_an_option_it_does_not_know_cannot_kill_it(self):
|
||||
"""Les arguments d'Odoo nous traversent : l'un d'eux ressemblera un
|
||||
jour à une option inconnue, et `parse_args` sortirait par SystemExit.
|
||||
"""
|
||||
vrai = warmup.sonder
|
||||
warmup.sonder = lambda *a, **k: True
|
||||
try:
|
||||
self.assertEqual(warmup.main(["--quiet", "--option-a-odoo"]), 0)
|
||||
finally:
|
||||
warmup.sonder = vrai
|
||||
|
||||
def test_quiet_says_nothing_at_all(self):
|
||||
import contextlib
|
||||
|
||||
vrai = warmup.sonder
|
||||
warmup.sonder = lambda *a, **k: True
|
||||
sortie = io.StringIO()
|
||||
try:
|
||||
with contextlib.redirect_stdout(sortie):
|
||||
warmup.main(["--quiet"])
|
||||
finally:
|
||||
warmup.sonder = vrai
|
||||
self.assertEqual(sortie.getvalue(), "")
|
||||
|
||||
|
||||
class TestHowRunShWiresIt(unittest.TestCase):
|
||||
"""Une option qu'Odoo ne connaît pas le tue au démarrage."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.source = RUN_SH.read_text(encoding="utf-8")
|
||||
|
||||
def test_the_flag_is_stripped_and_never_forwarded(self):
|
||||
bloc = self.source[
|
||||
self.source.index("--erplibre-disable-warmup-http)") :
|
||||
][:120]
|
||||
self.assertNotIn("EL_ARGS+=", bloc)
|
||||
|
||||
def test_no_http_disables_it_but_still_reaches_odoo(self):
|
||||
"""Rien à réveiller, mais Odoo doit garder son option."""
|
||||
bloc = self.source[
|
||||
self.source.index("--no-http|--stop-after-init)") :
|
||||
][:200]
|
||||
self.assertIn("EL_WARMUP=0", bloc)
|
||||
self.assertIn('EL_ARGS+=("$1")', bloc)
|
||||
|
||||
def test_it_runs_in_parallel(self):
|
||||
lancement = [
|
||||
texte
|
||||
for texte in self.source.splitlines()
|
||||
if texte.strip().endswith("&") and "2>/dev/null" in texte
|
||||
]
|
||||
self.assertTrue(lancement, "le réveil ne part pas en arrière-plan")
|
||||
|
||||
def test_it_does_not_survive_the_server(self):
|
||||
self.assertIn("trap", self.source)
|
||||
self.assertIn("EL_WARMUP_PID", self.source)
|
||||
|
||||
def test_its_own_errors_are_swallowed(self):
|
||||
self.assertIn("2>/dev/null &", self.source)
|
||||
|
||||
def test_odoo_arguments_are_separated_by_a_double_dash(self):
|
||||
"""Sans `--`, un « -c » destiné à Odoo serait lu comme le nôtre."""
|
||||
self.assertIn('-- "${EL_ARGS[@]}" 2>/dev/null &', self.source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue