diff --git a/.claude/skills/erplibre-commands/SKILL.md b/.claude/skills/erplibre-commands/SKILL.md index 1287bb7..0c5ff0d 100644 --- a/.claude/skills/erplibre-commands/SKILL.md +++ b/.claude/skills/erplibre-commands/SKILL.md @@ -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 diff --git a/conf/make.test.Makefile b/conf/make.test.Makefile index b636aa6..30625a9 100644 --- a/conf/make.test.Makefile +++ b/conf/make.test.Makefile @@ -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 diff --git a/mobile/compile_and_run.sh b/mobile/compile_and_run.sh index 6553a7f..151eca1 100755 --- a/mobile/compile_and_run.sh +++ b/mobile/compile_and_run.sh @@ -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 diff --git a/script/test/run_unit_test.sh b/script/test/run_unit_test.sh new file mode 100755 index 0000000..fb227ea --- /dev/null +++ b/script/test/run_unit_test.sh @@ -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} diff --git a/script/todo/todo.py b/script/todo/todo.py index 641dd66..04f9964 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -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é. diff --git a/test/test_mobile_bundle_transfer.py b/test/test_mobile_bundle_transfer.py index a89e490..f877e33 100644 --- a/test/test_mobile_bundle_transfer.py +++ b/test/test_mobile_bundle_transfer.py @@ -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