fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
5.2 KiB
Python
114 lines
5.2 KiB
Python
"""Produkt składa się z modułów — i węższe złożenie naprawdę działa.
|
|
|
|
Ekrany mieszkają w `app/ekrany/*.py`, moduły opcjonalne w `app/moduly/*`, a
|
|
`main.py` jest samym złożeniem. Sens tego podziału jest jeden: z tego samego
|
|
kodu ma dać się zbudować WĘŻSZY produkt, w którym pozostałych ekranów nie ma —
|
|
nie są wyłączone, tylko ich nie ma.
|
|
|
|
Test buduje takie złożenie naprawdę: kopiuje drzewo, usuwa moduły, uruchamia
|
|
aplikację w osobnym procesie i sprawdza, co się stało. Robi to w podprocesie,
|
|
bo importy Pythona są zapamiętywane w procesie — sprawdzanie „aplikacji bez
|
|
modułu" w tym samym procesie, w którym moduł już się zaimportował, dawałoby
|
|
wynik fałszywie pozytywny.
|
|
|
|
Czego ten test NIE sprawdza: czy w drzewie nie ma wzmianek o nieobecnych
|
|
ekranach. To ma sens dopiero wobec KONKRETNEGO produktu (wiadomo wtedy, których
|
|
ekranów nie ma) i tam jest sprawdzane — patrz zapory słownikowe.
|
|
"""
|
|
import json
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
|
|
# Złożenie węższe: same funkcje astrologiczne, bez raportu zbiorczego,
|
|
# bez administracji, bez generowania i bez wynoszenia treści na zewnątrz.
|
|
EKRANY_WASKIE = ["horoskop", "interpretacje", "kalendarz", "logowanie",
|
|
"lokalizacja", "sygnifikatory", "synastria"]
|
|
USUWANE = ["ekrany/skompiluj.py", "ekrany/ustawienia.py", "ekrany/konta.py",
|
|
"ekrany/pliki.py", "moduly/dodatki", "moduly/eksport"]
|
|
|
|
SONDA = '''
|
|
import json, os, tempfile
|
|
os.environ.update(ACCOUNTS_FILE=tempfile.mkdtemp()+"/a.json", APP_USER="szef",
|
|
APP_PASSWORD="t", RATE_LIMIT_PER_MIN="0",
|
|
SESSION_SECRET="t"*64, COOKIES_INSECURE="1")
|
|
from starlette.testclient import TestClient
|
|
from app.main import app
|
|
from app import security, session, features
|
|
c = TestClient(app, follow_redirects=False)
|
|
H = {"Cookie": f"{session.COOKIE}={security.issue_session('szef')}"}
|
|
wynik = {
|
|
"ekrany": [f.label for f in features.SCREENS],
|
|
"rozszerzenia": [f.label for f in features.EXTRAS],
|
|
"dziala": {a: c.get(a, headers=H).status_code
|
|
for a in ("/", "/interpret", "/timeline", "/synastry", "/significators")},
|
|
"nie_ma": {a: c.get(a, headers=H).status_code
|
|
for a in ("/compile", "/settings", "/accounts", "/files")},
|
|
# Liczymy odsyłacze WEWNĄTRZ paska nawigacji. Zliczanie wszystkich na stronie
|
|
# łapało też przypis OSM z wyszukiwarki miejsca — czyli test mierzyłby
|
|
# co innego, niż nazwa mu każe.
|
|
"nawigacja": (lambda h: h.split("<nav>")[1].split("</nav>")[0].count('<a href="'))(
|
|
c.get("/", headers=H).text),
|
|
}
|
|
print("WYNIK:" + json.dumps(wynik))
|
|
'''
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def waskie(tmp_path_factory):
|
|
korzen = tmp_path_factory.mktemp("waskie")
|
|
shutil.copytree(APP, korzen / "app",
|
|
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
|
for sciezka in USUWANE:
|
|
cel = korzen / "app" / sciezka
|
|
shutil.rmtree(cel) if cel.is_dir() else cel.unlink()
|
|
(korzen / "app" / "main.py").write_text(
|
|
"from app import rozszerzenia\n"
|
|
"from app.podstawa import _build_utc, _logic_error, app, logic, templates\n"
|
|
f"from app.ekrany import {', '.join(EKRANY_WASKIE)} # noqa: F401\n"
|
|
"rozszerzenia.zamontuj(app, logic=logic, templates=templates,\n"
|
|
" buduj_utc=_build_utc, blad=_logic_error)\n",
|
|
encoding="utf-8")
|
|
wynik = subprocess.run([sys.executable, "-c", SONDA], cwd=korzen,
|
|
capture_output=True, text=True,
|
|
env={"PYTHONPATH": str(korzen), "PATH": "/usr/bin:/bin"})
|
|
linia = next((l for l in wynik.stdout.splitlines() if l.startswith("WYNIK:")), None)
|
|
assert linia, f"sonda nie wystartowała:\n{wynik.stdout[-2000:]}\n{wynik.stderr[-2000:]}"
|
|
return json.loads(linia[len("WYNIK:"):])
|
|
|
|
|
|
def test_the_narrow_build_starts_at_all(waskie):
|
|
"""Najpierw to: aplikacja bez połowy modułów ma w ogóle wstać."""
|
|
assert waskie["dziala"]["/"] == 200
|
|
|
|
|
|
def test_every_screen_it_kept_works(waskie):
|
|
assert all(kod == 200 for kod in waskie["dziala"].values()), waskie["dziala"]
|
|
|
|
|
|
def test_every_screen_it_dropped_is_gone_not_merely_closed(waskie):
|
|
"""404, nie 403 i nie 500: ekran ma NIE ISTNIEĆ, a nie być odmówiony."""
|
|
assert all(kod == 404 for kod in waskie["nie_ma"].values()), waskie["nie_ma"]
|
|
|
|
|
|
def test_the_catalogue_describes_this_build_not_some_other(waskie):
|
|
"""Katalog funkcji powstaje ze zgłoszeń, więc opisuje ten obraz.
|
|
|
|
Wcześniej lista była wpisana na sztywno i obraz bez części ekranów i tak
|
|
niósł ich nazwy — czyli spis funkcji, których nie ma jak włączyć."""
|
|
assert set(waskie["ekrany"]) == {"Horoskop", "Interpretacje", "Kalendarz",
|
|
"Synastria", "Sygnifikatory"}
|
|
assert "Eksport plików" not in waskie["rozszerzenia"]
|
|
assert "Generowanie tekstu przez model" not in waskie["rozszerzenia"]
|
|
|
|
|
|
def test_navigation_has_no_dead_links(waskie):
|
|
"""Pozycje administracyjne też idą z rejestru — wpisany na sztywno odsyłacz
|
|
do ekranu kont zostawał w produkcie, który tego ekranu nie ma."""
|
|
assert waskie["nawigacja"] == len(waskie["ekrany"])
|