Spaces:
Runtime error
Runtime error
Download tests/test_deployment/test_docker_image.py from rahim237cm/jurix-api: direct link, hf CLI and curl.
- Browser
- Download file 7.62 kB
-
https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/tests/test_deployment/test_docker_image.py
- Command line
-
hf download hf://spaces/rahim237cm/jurix-api/tests/test_deployment/test_docker_image.py
-
curl -L -o test_docker_image.py https://huggingface.co/spaces/rahim237cm/jurix-api/resolve/main/tests/test_deployment/test_docker_image.py
7.62 kB
| """ | |
| Tests du Dockerfile et du contexte de build. | |
| Ils ne construisent pas l'image — trop lent pour une suite — mais verifient les | |
| proprietes dont l'absence se paie en production : | |
| - sans poppler, GET /laws/{id}/page/{n} repond 500 alors qu'il fonctionne en | |
| developpement, ou le binaire est installe sur la machine ; | |
| - sans .dockerignore, le COPY . . embarque .env, donc les cles d'API, dans une | |
| couche de l'image ; | |
| - sans migration au demarrage, le conteneur sert un schema incomplet : les | |
| colonnes search_vector, les index GIN et les declencheurs n'existent que dans | |
| alembic, jamais dans Base.metadata ; | |
| - sans versions figees, l'image prend les dernieres versions du jour de sa | |
| construction, que la suite n'a jamais vues. | |
| """ | |
| import pathlib | |
| import re | |
| from importlib import metadata | |
| import pytest | |
| ROOT = pathlib.Path(__file__).resolve().parents[2] | |
| DOCKERFILE = (ROOT / "Dockerfile").read_text(encoding="utf-8") | |
| DOCKERIGNORE_PATH = ROOT / ".dockerignore" | |
| REQUIREMENTS = (ROOT / "requirements.txt").read_text(encoding="utf-8") | |
| CONSTRAINTS_PATH = ROOT / "constraints.txt" | |
| COPIE_DU_CODE = "COPY --chown=jurix:jurix . ." | |
| def _nom(nom: str) -> str: | |
| """Nom de paquet normalise (PEP 503) : SQLAlchemy, sqlalchemy et SQL_Alchemy se valent.""" | |
| return re.sub(r"[-_.]+", "-", nom).lower() | |
| def _requirements(): | |
| from packaging.requirements import Requirement | |
| lignes = (ligne.split("#")[0].strip() for ligne in REQUIREMENTS.splitlines()) | |
| return [Requirement(ligne) for ligne in lignes if ligne] | |
| def _contraintes() -> dict: | |
| lignes = CONSTRAINTS_PATH.read_text(encoding="utf-8").splitlines() | |
| paires = (ligne.split("==", 1) for ligne in lignes if ligne and not ligne.startswith("#")) | |
| return {_nom(nom): version for nom, version in paires} | |
| class TestSystemDependencies: | |
| def test_package_is_installed(self, package): | |
| assert package in DOCKERFILE, ( | |
| f"{package} absent du Dockerfile : la fonctionnalite qui en depend " | |
| f"marchera en developpement et echouera en production." | |
| ) | |
| def test_tesseract_path_is_set_for_linux(self): | |
| assert "TESSERACT_PATH=/usr/bin/tesseract" in DOCKERFILE | |
| class TestBuildContext: | |
| def test_dockerignore_exists(self): | |
| assert DOCKERIGNORE_PATH.is_file(), ( | |
| "Sans .dockerignore, COPY . . embarque .env dans l'image." | |
| ) | |
| def test_excludes_what_must_not_ship(self, pattern): | |
| content = DOCKERIGNORE_PATH.read_text(encoding="utf-8") | |
| lines = {line.strip() for line in content.splitlines()} | |
| assert pattern in lines, f"{pattern} devrait etre exclu du contexte de build" | |
| def test_dockerignore_est_versionne(self): | |
| """ | |
| Il etait ignore par le .gitignore (*.dockerignore) : absent de tout | |
| checkout et de `git archive`, il ne protegeait que la machine ou il | |
| avait ete ecrit. Le build sur la VM aurait embarque le .env de | |
| production. | |
| """ | |
| import subprocess | |
| try: | |
| resultat = subprocess.run( | |
| ["git", "check-ignore", "-q", ".dockerignore"], cwd=ROOT, capture_output=True | |
| ) | |
| except FileNotFoundError: | |
| pytest.skip("git absent") | |
| if resultat.returncode == 128: | |
| pytest.skip("hors d'un depot git") | |
| assert resultat.returncode == 1, ".dockerignore est ignore par git" | |
| def test_env_example_stays_included(self): | |
| """Le modele de configuration, lui, doit rester : il documente les cles.""" | |
| content = DOCKERIGNORE_PATH.read_text(encoding="utf-8") | |
| assert "!.env.example" in content | |
| class TestStartup: | |
| def test_migrations_run_before_the_server(self): | |
| assert "alembic upgrade head" in DOCKERFILE | |
| upgrade_at = DOCKERFILE.index("alembic upgrade head") | |
| uvicorn_at = DOCKERFILE.index("uvicorn app.main:app", upgrade_at) | |
| assert upgrade_at < uvicorn_at | |
| def test_runs_as_a_non_root_user(self): | |
| assert "USER jurix" in DOCKERFILE | |
| def test_pas_de_journal_d_acces(self): | |
| """Il ecrivait le jeton d'administration du WebSocket (?token=...) en clair.""" | |
| assert "--no-access-log" in DOCKERFILE | |
| class TestCouches: | |
| def test_le_modele_fasttext_precede_le_code(self): | |
| """ | |
| Telecharge apres la copie du code, le modele (126 Mo) l'etait a chaque | |
| mise a jour ; et un echec donnait, sans erreur, une image sans | |
| detection de langue. | |
| """ | |
| assert DOCKERFILE.index("lid.176.bin") < DOCKERFILE.index(COPIE_DU_CODE) | |
| assert "skip language detection" not in DOCKERFILE | |
| def test_pas_de_chown_recursif_apres_le_code(self): | |
| """Il recopiait tout /app, modele compris, dans une couche de plus.""" | |
| assert "chown -R" not in DOCKERFILE.split(COPIE_DU_CODE, 1)[1] | |
| class TestVersionsFigees: | |
| """ | |
| requirements.txt donne des fourchettes. Sans contraintes, l'image prenait | |
| les dernieres versions du jour de sa construction : SQLAlchemy 2.1, sorti | |
| entre deux, n'installait plus greenlet, et l'API plantait au demarrage | |
| alors que tout passait en developpement, en 2.0.52. | |
| """ | |
| # Outils d'installation, et non code de l'application : le venv peut avoir | |
| # les siens (conda), que scripts/figer_versions.sh laisse libres. | |
| OUTILS = {"pip", "setuptools", "wheel", "packaging"} | |
| def test_l_image_installe_les_versions_figees(self): | |
| assert "COPY requirements.txt constraints.txt ./" in DOCKERFILE | |
| assert "-r requirements.txt -c constraints.txt" in DOCKERFILE | |
| def test_chaque_dependance_est_figee(self): | |
| contraintes = _contraintes() | |
| absentes = [r.name for r in _requirements() if _nom(r.name) not in contraintes] | |
| assert not absentes, ( | |
| f"Absentes de constraints.txt : {absentes}. Lancer scripts/figer_versions.sh." | |
| ) | |
| def test_les_versions_figees_respectent_requirements(self): | |
| contraintes = _contraintes() | |
| hors = [ | |
| f"{r.name}=={contraintes[_nom(r.name)]} hors de {r.specifier}" | |
| for r in _requirements() | |
| if _nom(r.name) in contraintes and contraintes[_nom(r.name)] not in r.specifier | |
| ] | |
| assert not hors, f"{hors}. Lancer scripts/figer_versions.sh." | |
| def test_greenlet_est_installe(self): | |
| """sqlalchemy.ext.asyncio en a besoin, et SQLAlchemy 2.1 ne l'installe plus.""" | |
| sqlalchemy = next(r for r in _requirements() if _nom(r.name) == "sqlalchemy") | |
| assert "asyncio" in sqlalchemy.extras | |
| assert "greenlet" in _contraintes() | |
| def test_la_suite_tourne_sur_les_versions_de_l_image(self): | |
| """Ce que les tests valident doit etre ce que l'image installe.""" | |
| ecarts = [] | |
| for nom, version in _contraintes().items(): | |
| if nom in self.OUTILS: | |
| continue | |
| try: | |
| installee = metadata.version(nom) | |
| except metadata.PackageNotFoundError: | |
| continue | |
| if installee != version: | |
| ecarts.append(f"{nom} : venv {installee}, image {version}") | |
| assert not ecarts, ( | |
| f"{ecarts}. Aligner le venv (pip install -r requirements.txt -c constraints.txt) " | |
| f"ou, apres une mise a jour voulue, lancer scripts/figer_versions.sh." | |
| ) | |