Tests von 55 auf 12 Sekunden, Abhängigkeiten nur noch einmal installieren
Docker-Image bauen / build (push) Successful in 1m7s
Docker-Image bauen / build (push) Successful in 1m7s
Der Testlauf war zu langsam. Gemessen statt geraten: 0,68 s Vorbereitung pro Test, bei 81 Tests praktisch die ganze Laufzeit. Ursache war bcrypt. Pro Test wird ein Hash gebildet und geprüft, und das ist absichtlich langsam - genau das soll es im Betrieb sein. Der Aufwand ist jetzt über BCRYPT_ROUNDS einstellbar (Standard bleibt 12) und in den Tests auf 4 gesetzt: dort geht es um die Ablauflogik, nicht um die Stärke des Hashes. Der Dekompressionsbomben-Test erzeugte ein Bild mit 400 Megapixeln, allein dafür 4,8 s. Jetzt wird stattdessen die Grenze heruntergesetzt und ein kleines Bild verwendet - dieselbe Codestelle, ohne die Wartezeit. Ausserdem installierten Test- und Anwendungs-Image dieselben Abhängigkeiten zweimal. Dockerfile.test setzt nun auf dem gebauten Anwendungs-Image auf, der Testschritt kommt entsprechend danach. Dabei zwei Fallen, die beide auffielen, weil die Tests plötzlich wieder langsam waren: - Das Basis-Image bringt seinen eigenen Stand von app/ mit. Ohne erneutes Kopieren prüfen die Tests den Code des Basis-Images - ist es veraltet, läuft alles gegen alten Code und meldet Erfolg. - Der ENTRYPOINT des Anwendungs-Images startet eine Datenbank-Migration. Für Tests weder nötig noch erwünscht, darum geleert. Ganze Kette lokal durchgespielt: bauen, testen, Image prüfen - 22 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
@@ -25,16 +25,6 @@ jobs:
|
||||
- name: Quellcode auschecken
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
set -eu
|
||||
# Über den Build-Kontext statt per Bind-Mount: läuft der Runner
|
||||
# selbst in einem Container, zeigt $PWD auf einen Pfad, den der
|
||||
# Docker-Daemon des Hosts nicht kennt - der Mount wäre leer und
|
||||
# die Tests liefen ohne Fehlermeldung ins Leere.
|
||||
docker build -f Dockerfile.test -t kleiderboerse-tests .
|
||||
docker run --rm kleiderboerse-tests
|
||||
|
||||
- name: Version bestimmen
|
||||
id: meta
|
||||
env:
|
||||
@@ -79,6 +69,18 @@ jobs:
|
||||
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
|
||||
.
|
||||
|
||||
# Nach dem Bauen: das Test-Image setzt auf dem eben gebauten auf und
|
||||
# spart sich damit ein zweites Mal Abhängigkeiten installieren. Den
|
||||
# Quellcode kopiert es trotzdem frisch hinein, sonst prüfte es den
|
||||
# Stand des Basis-Images.
|
||||
- name: Tests
|
||||
run: |
|
||||
set -eu
|
||||
docker build -f Dockerfile.test \
|
||||
--build-arg "BASIS=$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.version }}" \
|
||||
-t kleiderboerse-tests .
|
||||
docker run --rm kleiderboerse-tests
|
||||
|
||||
# Prüft das fertige Image, bevor irgendetwas veröffentlicht wird.
|
||||
# Geprüft wird im Container selbst (kein Port-Mapping nötig, damit der
|
||||
# Test unabhängig vom Netzwerk-Setup des Runners funktioniert).
|
||||
|
||||
+19
-6
@@ -1,13 +1,26 @@
|
||||
# Nur zum Testen - baut die Testumgebung samt Quellcode ins Image.
|
||||
# Testumgebung - setzt auf dem fertigen Anwendungs-Image auf.
|
||||
#
|
||||
# Vorteil: die Abhängigkeiten sind dort schon installiert und werden nicht
|
||||
# ein zweites Mal geholt. Die Tests laufen ausserdem gegen genau die
|
||||
# Fassungen, die auch ausgeliefert werden.
|
||||
#
|
||||
# Der Quellcode wird trotzdem NEU hineinkopiert. Ohne das würden die Tests
|
||||
# gegen den Stand des Basis-Images laufen - ist es veraltet, prüfen sie
|
||||
# stillschweigend alten Code und melden Erfolg.
|
||||
#
|
||||
# ENTRYPOINT wird geleert: der des Anwendungs-Images führt eine
|
||||
# Datenbank-Migration aus, die für die Tests weder nötig noch erwünscht ist.
|
||||
#
|
||||
# Bewusst ohne Bind-Mount: läuft der CI-Runner selbst in einem Container,
|
||||
# zeigt $PWD auf einen Pfad, den der Docker-Daemon des Hosts nicht kennt.
|
||||
# Der Mount wäre dann leer und die Tests liefen ins Leere - ohne
|
||||
# Fehlermeldung. Über den Build-Kontext kommt der Code zuverlässig hinein.
|
||||
FROM python:3.11-slim
|
||||
# Der Mount wäre leer und die Tests liefen ohne Fehlermeldung ins Leere.
|
||||
ARG BASIS=kleiderboerse-app
|
||||
FROM ${BASIS}
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt requirements-dev.txt ./
|
||||
USER root
|
||||
ENTRYPOINT []
|
||||
|
||||
COPY requirements-dev.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements-dev.txt
|
||||
|
||||
COPY alembic.ini ./
|
||||
|
||||
@@ -50,6 +50,12 @@ class Einstellungen(BaseSettings):
|
||||
limit_reservieren: str = Field(default="5/hour", validation_alias="RATE_LIMIT_RESERVE")
|
||||
limit_anmelden: str = Field(default="10/hour", validation_alias="RATE_LIMIT_LOGIN")
|
||||
|
||||
# Rechenaufwand für das Passwort-Hashing. 12 ist der sinnvolle
|
||||
# Standard; bcrypt ist absichtlich langsam, damit sich Passwörter
|
||||
# nicht durchprobieren lassen. In Tests wird der Wert gesenkt -
|
||||
# sonst kostet jeder Test rund eine halbe Sekunde nur dafür.
|
||||
bcrypt_runden: int = Field(default=12, validation_alias="BCRYPT_ROUNDS")
|
||||
|
||||
# Nach dieser Zeit ohne Aktivität wird abgemeldet.
|
||||
sitzung_max_ruhe_sekunden: int = Field(default=7200,
|
||||
validation_alias="SESSION_IDLE_SECONDS")
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ limiter = Limiter(key_func=get_remote_address)
|
||||
# ------------------------------------------------------------- Passwort ---
|
||||
|
||||
def passwort_hash(klartext: str) -> str:
|
||||
return bcrypt.hashpw(klartext.encode(), bcrypt.gensalt()).decode()
|
||||
runden = einstellungen().bcrypt_runden
|
||||
return bcrypt.hashpw(klartext.encode(), bcrypt.gensalt(runden)).decode()
|
||||
|
||||
|
||||
def passwort_stimmt(klartext: str, hash_wert: str) -> bool:
|
||||
|
||||
@@ -17,6 +17,10 @@ TEST_PASSWORT = "test-passwort-123"
|
||||
os.environ.setdefault("COOKIE_HTTPS_ONLY", "false")
|
||||
os.environ.setdefault("ADMIN_PASSWORD", TEST_PASSWORT)
|
||||
os.environ.setdefault("SECRET_KEY", "nur-fuer-tests")
|
||||
# bcrypt ist absichtlich langsam. Bei rund 160 Hash-Vorgängen über
|
||||
# alle Tests kostete der Standardaufwand knapp eine Minute; hier
|
||||
# geht es um die Ablauflogik, nicht um die Stärke des Hashes.
|
||||
os.environ.setdefault("BCRYPT_ROUNDS", "4")
|
||||
|
||||
import pytest # noqa: E402
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
+13
-7
@@ -127,21 +127,27 @@ def test_zu_grosse_datei_wird_abgewiesen(client, item_id, monkeypatch):
|
||||
einstellungen.cache_clear()
|
||||
|
||||
|
||||
def test_dekompressionsbombe_wird_abgewiesen(client, item_id):
|
||||
"""Ein paar hundert Kilobyte, die beim Entpacken den Speicher füllen.
|
||||
def test_dekompressionsbombe_wird_abgewiesen(client, item_id, monkeypatch):
|
||||
"""Ein paar Kilobyte, die beim Entpacken den Speicher füllen.
|
||||
|
||||
Pillow allein würde hier nur warnen - der Fehler kommt erst bei der
|
||||
doppelten Pixelzahl. Darum die ausdrückliche Prüfung in images.py.
|
||||
|
||||
Statt wirklich 400 Megapixel zu erzeugen (was den Test um Sekunden
|
||||
verlängerte) wird die Grenze heruntergesetzt: geprüft wird dieselbe
|
||||
Codestelle, nur eben mit einem kleinen Bild.
|
||||
"""
|
||||
from app import images
|
||||
|
||||
monkeypatch.setattr(images, "MAX_PIXEL", 100_000)
|
||||
|
||||
puffer = io.BytesIO()
|
||||
# 20000 x 20000 = 400 Megapixel, als einfarbiges PNG winzig komprimiert
|
||||
Image.new("RGB", (20000, 20000), (0, 0, 0)).save(puffer, "PNG", compress_level=9)
|
||||
roh = puffer.getvalue()
|
||||
assert len(roh) < 2 * 1024 * 1024, "Testbild sollte klein sein"
|
||||
# 600 x 600 = 360'000 Bildpunkte, also über der gesetzten Grenze
|
||||
Image.new("RGB", (600, 600), (0, 0, 0)).save(puffer, "PNG", compress_level=9)
|
||||
|
||||
antwort = client.post(
|
||||
f"/api/v1/items/{item_id}/images",
|
||||
files={"dateien": ("bombe.png", roh, "image/png")},
|
||||
files={"dateien": ("bombe.png", puffer.getvalue(), "image/png")},
|
||||
)
|
||||
assert antwort.status_code == 422
|
||||
assert "Bildpunkte" in antwort.json()["detail"]
|
||||
|
||||
Reference in New Issue
Block a user