[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée

Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.

La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.

Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.

--- EN ---

These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.

The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.

Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-21 20:06:23 -04:00
parent deae7d16c7
commit 5b748e347e
6 changed files with 208 additions and 7 deletions

View file

@ -34,7 +34,17 @@ make db_clone_test_to_test2 # Cloner test -> test2
```
## Tests
### Suite complète
### Unitaires python (rapides, sans base de données)
```bash
make test_unit # tout ce qui tourne sans DB ni Odoo
make test_unit_file F=test/test_qemu_motd.py # un seul fichier
```
Ils lisent le code et exécutent les fragments de shell générés par `todo.py`,
avec `sudo`, `pgrep` et `pkill` bouchonnés — quelques secondes. Le lanceur
annonce sa dépendance à `mobile/erplibre_home_mobile` : absent, les tests du
transfert mobile se déclarent ignorés au lieu de passer en silence.
### Suite complète (base de données requise)
```bash
make test # Tests de base + format
make test_full_fast # Tests complets en parallèle

View file

@ -4,6 +4,31 @@
# TODO load specific test file : ./run.sh -d test_file --log-level=test --test-enable --stop-after-init --test-file ./.venv.erplibre/test.py
###########################
# Tests unitaires python #
###########################
# Ni base de données, ni Odoo, ni VM : ils lisent le code et exécutent les
# fragments de shell que todo.py génère, « sudo », « pgrep » et « pkill »
# bouchonnés. Une dizaine de secondes, là où « make test » demande une base et
# plusieurs minutes — d'où une cible à part, faite pour être lancée souvent.
#
# DÉPENDANCE : les tests du transfert mobile lisent
# mobile/erplibre_home_mobile. Le lanceur l'annonce présent ou absent avant de
# commencer, et les tests concernés se déclarent ignorés plutôt que de passer
# en silence — un test vert sans son dépôt ne prouve rien.
#
# « test_todo.py » n'en fait PAS partie : il demande une base de données. Son
# nom sans souligné le laisse hors du motif, et c'est voulu.
.PHONY: test_unit
test_unit:
./script/test/run_unit_test.sh
# Un seul fichier, pour la boucle d'écriture :
# make test_unit_file F=test/test_qemu_forgejo.py
.PHONY: test_unit_file
test_unit_file:
./script/test/run_unit_test.sh $(F)
.PHONY: open_test_coverage
open_test_coverage:
-$(BROWSER) htmlcov/index.html

View file

@ -5,10 +5,26 @@ if [[ ! -d "./mobile/erplibre_home_mobile" ]]; then
exit 1
fi
cd mobile/erplibre_home_mobile
WORKSPACE="$(pwd)"
cd mobile/erplibre_home_mobile || exit 1
npm install
npm run build && npx cap sync
npm run build || exit 1
# Le transfert des dépôts du manifeste DANS l'application est ce qui fait
# l'intérêt de son navigateur de code hors ligne, et il peut être vide sans que
# la compilation le dise. Ces dépôts entrent en packs — un APK est un ZIP borné
# à 65535 entrées, quand un fichier par source en réclamait 123 678 — et le
# vérificateur relit un échantillon depuis les tranches pour le comparer à la
# source. Trois pannes qu'un « build OK » passe sous silence : transfert vide,
# tranche absente, index qui promet des octets que sa tranche n'a pas.
#
# Même vérification que l'installation d'une VM, même script : une seule
# autorité.
"${WORKSPACE}/script/mobile/check_bundle_transfer.py" . --workspace "${WORKSPACE}" || exit 1
npx cap sync || exit 1
npx cap run android
cd -
cd - || exit 1

72
script/test/run_unit_test.sh Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Tests unitaires python du dépôt : ni base de données, ni Odoo, ni VM.
#
# Ils lisent le code et exécutent les fragments de shell que todo.py génère,
# avec « sudo », « pgrep » et « pkill » bouchonnés — c'est ce qui les rend
# lançables partout et en quelques secondes, là où « make test » demande une
# base et plusieurs minutes.
#
# DÉPENDANCE DÉCLARÉE : les tests du transfert mobile lisent
# mobile/erplibre_home_mobile. Absent, ils se disent ignorés plutôt que de
# passer en silence — un test vert sans son dépôt ne prouve rien. Ce script
# l'annonce donc avant de commencer.
#
# ./script/test/run_unit_test.sh [fichiers...]
set -uo pipefail
Red='\033[0;31m'
Green='\033[0;32m'
Yellow='\033[0;33m'
Color_Off='\033[0m'
cd "$(dirname "$0")/../.." || exit 1
PY=./.venv.erplibre/bin/python
if [[ ! -x "${PY}" ]]; then
echo -e "${Red}✗ ${PY} absent : lancer l'installation ERPLibre d'abord.${Color_Off}"
exit 1
fi
MOBILE=mobile/erplibre_home_mobile
if [[ -d "${MOBILE}" ]]; then
echo -e " dépendance ${MOBILE} : ${Green}présente${Color_Off}"
else
echo -e " dépendance ${MOBILE} : ${Yellow}absente${Color_Off}"
echo " (les tests du transfert mobile s'en passeront et le diront)"
fi
FILES=("$@")
if [[ ${#FILES[@]} -eq 0 ]]; then
# Aucun argument : tout ce que le dépôt sait tester sans base de données.
mapfile -t FILES < <(ls test/test_qemu_*.py test/test_mobile_*.py \
test/test_todo_*.py 2>/dev/null)
fi
fail=0
total=0
for f in "${FILES[@]}"; do
out=$(PYTHONPATH=. "${PY}" "${f}" 2>&1)
ran=$(echo "${out}" | grep -oE 'Ran [0-9]+' | grep -oE '[0-9]+' | tail -1)
skipped=$(echo "${out}" | grep -oE 'skipped=[0-9]+' | tail -1)
if echo "${out}" | grep -qE '^OK'; then
state="${Green}OK${Color_Off}"
else
state="${Red}ÉCHEC${Color_Off}"
fail=1
fi
total=$((total + ${ran:-0}))
printf " %-42s %5s tests %-14s %b\n" \
"$(basename "${f}")" "${ran:-?}" "${skipped:-}" "${state}"
[[ "${state}" == *"ÉCHEC"* ]] && echo "${out}" | tail -12
done
echo " ─────"
if [[ ${fail} -eq 0 ]]; then
echo -e " ${Green}${total} tests, tout vert${Color_Off}"
else
echo -e " ${Red}des échecs ci-dessus${Color_Off}"
fi
exit ${fail}

View file

@ -2058,13 +2058,17 @@ class TODO:
host, from_ssh = self._qemu_self_address()
user = os.environ.get("USER", "user")
print(f"\n {t('No display here; run this on YOUR workstation:')}")
print(f"\n virt-viewer -c qemu+ssh://{user}@{host}/system {domain}\n")
print(
f"\n virt-viewer -c qemu+ssh://{user}@{host}/system {domain}\n"
)
if not from_ssh:
print(f" ⚠ {t('Not in an SSH session: check the host address.')}")
print(f" {t('A ~/.ssh/config alias works there too.')}")
print(f" {t('It builds its own tunnel; no ssh -L to keep open.')}")
print(f" {t('Missing? Install virt-viewer:')} apt / dnf / pacman"
" / zypper")
print(
f" {t('Missing? Install virt-viewer:')} apt / dnf / pacman"
" / zypper"
)
def _qemu_console_tunnel(self, name, src):
"""Tunnel vers l'ÉCRAN QEMU d'une VM, pas vers un serveur de l'invité.

View file

@ -263,6 +263,80 @@ class TestTheCommandLine(unittest.TestCase):
self.assertNotIn("Traceback", buf.getvalue())
REPO = Path(__file__).resolve().parent.parent
MOBILE = REPO / "mobile" / "erplibre_home_mobile"
class TestTheRealBundle(unittest.TestCase):
"""Le VRAI transfert, quand le dépôt mobile est installé et compilé.
C'est ici que ces tests DÉCLARENT leur dépendance : sans
mobile/erplibre_home_mobile, ils se disent ignorés plutôt que de passer en
silence — un test vert sans son dépôt ne prouve rien. Le lanceur
(script/test/run_unit_test.sh) annonce la même dépendance avant de
commencer.
Ce qu'ils gardent : qu'une compilation réelle produise bien des PACKS. Un
retour au fichier-par-source ferait disparaître le champ « chunk » des
index, et la limite du ZIP reviendrait — 123 678 entrées pour un plafond de
65 535, silencieusement, jusqu'à l'APK.
"""
@classmethod
def setUpClass(cls):
if not MOBILE.is_dir():
# Pas de « relative_to » : il lève quand le chemin sort du
# dépôt, et une erreur n'est pas un « ignoré » — mesuré en
# simulant l'absence.
raise unittest.SkipTest(
"mobile/erplibre_home_mobile absent :"
" ./mobile/install_mobile_dev.sh"
)
cls.repos = MOBILE / "dist" / "repos"
if not (cls.repos / "manifest.json").is_file():
raise unittest.SkipTest(
"dépôt mobile présent mais pas compilé :"
" ./mobile/compile_and_run.sh (ou npm run build)"
)
def test_the_transfer_is_coherent(self):
rep = cbt.check(MOBILE, REPO)
self.assertGreater(rep["repos"], 1)
self.assertGreater(rep["files"], cbt.MIN_FILES)
self.assertGreater(rep["packs"], 0)
def test_a_sample_matches_the_source(self):
"""La seule vérification qui prouve un transfert FIDÈLE."""
rep = cbt.check(MOBILE, REPO)
self.assertGreater(rep["compared"], 0)
def test_the_indexes_are_packed_not_file_per_source(self):
"""Le garde-fou de la limite du ZIP : chaque fichier doit porter sa
tranche. Sans « chunk », c'est un fichier par source, et l'APK sera
refusé — mais bien plus tard, et sans dire pourquoi."""
man = json.loads((self.repos / "manifest.json").read_text())
checked = 0
for proj in man[:5]:
index = self.repos / proj["slug"] / "index.json"
entries = json.loads(index.read_text())
files = [e for e in entries if e.get("type") == "file"]
if not files:
continue
self.assertTrue(
all("chunk" in e for e in files),
f"{proj['slug']} : des fichiers sans tranche",
)
checked += 1
self.assertGreater(checked, 0, "aucun dépôt à vérifier")
def test_no_bundled_test_file_lingers_as_a_source(self):
"""Effet de bord mesuré, et il compte : empaquetés, les 1 599 fichiers
de test des dépôts Odoo ne sont plus ramassés par vitest — 1 423
fichiers de test ramenés à 75, 35 s ramenées à 3 s."""
stray = list(self.repos.glob("*/**/*.test.ts"))
self.assertEqual([], stray)
import contextlib as _contextlib # noqa: E402
import unittest.mock # noqa: E402