[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:
parent
deae7d16c7
commit
5b748e347e
6 changed files with 208 additions and 7 deletions
|
|
@ -34,7 +34,17 @@ make db_clone_test_to_test2 # Cloner test -> test2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## 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
|
```bash
|
||||||
make test # Tests de base + format
|
make test # Tests de base + format
|
||||||
make test_full_fast # Tests complets en parallèle
|
make test_full_fast # Tests complets en parallèle
|
||||||
|
|
|
||||||
|
|
@ -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
|
# 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
|
.PHONY: open_test_coverage
|
||||||
open_test_coverage:
|
open_test_coverage:
|
||||||
-$(BROWSER) htmlcov/index.html
|
-$(BROWSER) htmlcov/index.html
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,26 @@ if [[ ! -d "./mobile/erplibre_home_mobile" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd mobile/erplibre_home_mobile
|
WORKSPACE="$(pwd)"
|
||||||
|
|
||||||
|
cd mobile/erplibre_home_mobile || exit 1
|
||||||
|
|
||||||
npm install
|
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
|
npx cap run android
|
||||||
|
|
||||||
cd -
|
cd - || exit 1
|
||||||
|
|
|
||||||
72
script/test/run_unit_test.sh
Executable file
72
script/test/run_unit_test.sh
Executable 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}
|
||||||
|
|
@ -2058,13 +2058,17 @@ class TODO:
|
||||||
host, from_ssh = self._qemu_self_address()
|
host, from_ssh = self._qemu_self_address()
|
||||||
user = os.environ.get("USER", "user")
|
user = os.environ.get("USER", "user")
|
||||||
print(f"\n {t('No display here; run this on YOUR workstation:')}")
|
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:
|
if not from_ssh:
|
||||||
print(f" ⚠ {t('Not in an SSH session: check the host address.')}")
|
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('A ~/.ssh/config alias works there too.')}")
|
||||||
print(f" {t('It builds its own tunnel; no ssh -L to keep open.')}")
|
print(f" {t('It builds its own tunnel; no ssh -L to keep open.')}")
|
||||||
print(f" {t('Missing? Install virt-viewer:')} apt / dnf / pacman"
|
print(
|
||||||
" / zypper")
|
f" {t('Missing? Install virt-viewer:')} apt / dnf / pacman"
|
||||||
|
" / zypper"
|
||||||
|
)
|
||||||
|
|
||||||
def _qemu_console_tunnel(self, name, src):
|
def _qemu_console_tunnel(self, name, src):
|
||||||
"""Tunnel vers l'ÉCRAN QEMU d'une VM, pas vers un serveur de l'invité.
|
"""Tunnel vers l'ÉCRAN QEMU d'une VM, pas vers un serveur de l'invité.
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,80 @@ class TestTheCommandLine(unittest.TestCase):
|
||||||
self.assertNotIn("Traceback", buf.getvalue())
|
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 contextlib as _contextlib # noqa: E402
|
||||||
import unittest.mock # noqa: E402
|
import unittest.mock # noqa: E402
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue