Files
astrololo/services/presentation/tests/test_ekrany_bez_sladu.py
T
gitea c1f5bea9f7
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
astrololo: ekrany i eksport jako moduły, katalog z rejestracji (3/5)
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>
2026-08-26 13:50:34 +02:00

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"])