10970c579f
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Failing after 0s
Testy / Testy astroklient (pull_request) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 7s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 19s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m26s
Testy / Testy astrodemo (push) Failing after 0s
Testy / Testy astroklient (push) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Trzeci produkt drabiny: astrodemo (dwie funkcje) → astroklient → astrololo. Po trzech poprzednich krokach jest cienki, bo jest ZŁOŻENIEM, a nie kopią: własne main.py z ośmioma importami, a rdzeń — ekrany, szablony, zasoby — bierze z warstwy prezentacji przy budowaniu obrazu. Jedno źródło, dwa produkty; inaczej te same 2500 linii szablonów żyłyby w dwóch egzemplarzach i rozjechały się w ciągu tygodni, po cichu. CO MA: Horoskop, Interpretacje, Kalendarz, Synastria, Sygnifikatory, wgrywanie plików. Wyszukiwarka miejsca i strefa czasowa zgodnie z ustaleniem. CZEGO NIE MA I DLACZEGO NIE DA SIĘ WŁĄCZYĆ: plików usuniętych wg usun.txt nie ma w obrazie. Nie istnieje uprawnienie, którym dałoby się je odsłonić, bo katalog funkcji składa się ze ZGŁOSZEŃ ekranów obecnych w obrazie. To dlatego „każde konto dostaje wszystko, co ta usługa umie" jest tu bezpieczne i nie wymaga wypisywania listy: zbiór liczy się z katalogu, więc opisuje ten produkt. KONTA jak w astrodemo: z konfiguracji środowiska (ASTROKLIENT_USERS), jeden poziom dostępu, bez pliku kont i bez ekranu ich zakładania. Konta rozdziela się po to, żeby każde miało własną pulę plików. PULE PER KONTO — tu była realna dziura. Warstwa logiczna przenosiła pulę tylko przy raporcie i operacjach na plikach, więc Kalendarz i Sygnifikatory czytałyby CAŁY udział: jedno konto widziałoby pliki drugiego, mimo obietnicy izolacji. Domknięte: TimelineRequest i QueryRequest niosą teraz pulę, a QueryService buduje klienta danych na żądanie. Pula jedzie w każdym żądaniu w dół i bierze się z kontekstu ustawianego przy wejściu, nigdy z formularza. Test podstawia `tenant=ktos-inny` w POST i sprawdza, że w dół poszedł login zalogowanego. WARSTWA WSPÓLNA ROZDZIELONA OD POJĘCIA ADMINISTRATORA. base.html miał wpisany na sztywno warunek `can(request, 'admin')` i odsyłacz do ekranu kont — czyli w produkcie bez tego ekranu zostawał martwy link i nazwa czegoś, czego nie ma. Rejestr niesie teraz wymagane uprawnienie, a szablon dostaje gotową listę. Podstawa przestała też importować moduły służące jednemu ekranowi (konta, stany plików), bo produkt bez tego ekranu wlókł ich zależności. ZAPORA SŁOWNIKOWA NAD REALNYM DRZEWEM. Test buduje złożenie tak samo jak Dockerfile i szuka słów o funkcjach, których nie ma — w odpowiedziach ORAZ w plikach. Pierwsza wersja znalazła dziesięć trafień, w tym trzy moje własne docstringi WYLICZAJĄCE nieobecne funkcje: zdanie „nie ma tu generowania tekstu" mówi wprost, że coś takiego istnieje, więc jest takim samym śladem jak przycisk. Po poprawkach: zero. Test ma kontrolę negatywną — podrzucony plik ma go wywrócić. usun.txt jest DANYMI, nie tekstem w Dockerfile: czyta go też test pilnujący, żeby zgadzał się ze złożeniem w main.py. Rozjazd znaczyłby albo martwy kod w obrazie, albo błąd dopiero przy uruchomieniu. CI: astroklient buduje się z KORZENIA repozytorium (jego Dockerfile sięga po rdzeń), a zmiana w warstwie prezentacji też go przebudowuje — bez tego jego obraz zostawałby ze starymi ekranami, a różnicy nie byłoby widać do zgłoszenia użytkownika. Testy: astroklient 12, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
261 lines
11 KiB
YAML
261 lines
11 KiB
YAML
name: Testy
|
|
|
|
# Odpala się przy każdym pushu (dowolna gałąź) oraz dla pull requestów do master.
|
|
on:
|
|
push:
|
|
pull_request:
|
|
branches: [master]
|
|
|
|
jobs:
|
|
logic-tests:
|
|
name: Testy warstwy logicznej (silnik)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Python 3.12 (jak w obrazach Dockera)
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: services/logic/requirements-dev.txt
|
|
|
|
# Jądro efemeryd JPL (de421.bsp, ~17 MB). Cache'ujemy je między runami.
|
|
- name: Cache jądra efemeryd
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: services/logic/.ephemeris
|
|
key: ephemeris-de421
|
|
|
|
# Pobieramy jawnie (a nie licząc na auto-pobranie przez Skyfield), żeby
|
|
# brak jądra był twardym błędem, a nie cichym pomijaniem testów.
|
|
- name: Pobierz jądro efemeryd (gdy brak w cache)
|
|
run: |
|
|
mkdir -p services/logic/.ephemeris
|
|
if [ ! -s services/logic/.ephemeris/de421.bsp ]; then
|
|
curl -fSL --retry 3 --max-time 300 \
|
|
-o services/logic/.ephemeris/de421.bsp \
|
|
https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/de421.bsp
|
|
fi
|
|
ls -lh services/logic/.ephemeris/de421.bsp
|
|
|
|
- name: Instalacja zależności
|
|
run: pip install -r services/logic/requirements-dev.txt
|
|
|
|
# CI=true (ustawiane przez GitHub) sprawia, że brak silnika = błąd,
|
|
# a nie pominięcie — patrz tests/conftest.py.
|
|
- name: Testy (pytest)
|
|
working-directory: services/logic
|
|
env:
|
|
PYTHONPATH: .
|
|
EPHEMERIS_DIR: ${{ github.workspace }}/services/logic/.ephemeris
|
|
run: pytest tests -q -rs
|
|
|
|
presentation-tests:
|
|
name: Testy warstwy prezentacji (dostęp do baz)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: services/presentation/requirements-dev.txt
|
|
- name: Instalacja zależności
|
|
run: pip install -r services/presentation/requirements-dev.txt
|
|
# Bramka chroniąca oryginalne bazy — nietestowany kod ochronny jest gorszy
|
|
# niż jego brak, bo daje złudzenie zabezpieczenia.
|
|
- name: Testy (pytest)
|
|
working-directory: services/presentation
|
|
env:
|
|
PYTHONPATH: .
|
|
run: pytest tests -q -rs
|
|
|
|
data-tests:
|
|
name: Testy warstwy bazodanowej (ochrona baz)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: services/data/requirements-dev.txt
|
|
- name: Instalacja zależności
|
|
run: pip install -r services/data/requirements-dev.txt
|
|
# Rekordy-pułapki (DAN-26): nietestowany kod ochronny jest gorszy niż jego
|
|
# brak, bo daje złudzenie zabezpieczenia. Ta warstwa dotąd nie miała testów.
|
|
- name: Testy (pytest)
|
|
working-directory: services/data
|
|
env:
|
|
PYTHONPATH: .
|
|
run: pytest tests -q -rs
|
|
|
|
astrodemo-tests:
|
|
name: Testy astrodemo
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: services/astrodemo/requirements-dev.txt
|
|
- name: Instalacja zależności
|
|
run: pip install -r services/astrodemo/requirements-dev.txt
|
|
# Demo rozmawia z warstwą danych PRODUKCJI, więc jego powierzchnia musi być
|
|
# pilnowana tak samo jak reszty: testy sprawdzają m.in., że nie przybyła
|
|
# żadna trasa poza dwiema funkcjami.
|
|
- name: Testy (pytest)
|
|
working-directory: services/astrodemo
|
|
astroklient-tests:
|
|
name: Testy astroklient
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: services/astroklient/requirements-dev.txt
|
|
- name: Instalacja zależności
|
|
run: pip install -r services/astroklient/requirements-dev.txt
|
|
# Testy budują drzewo tak samo, jak robi to Dockerfile: rdzeń z warstwy
|
|
# prezentacji, minus usun.txt, plus nakładka. Sprawdzają WYNIK złożenia,
|
|
# bo tylko on trafia do obrazu.
|
|
- name: Testy (pytest)
|
|
working-directory: services/astroklient
|
|
|
|
env:
|
|
PYTHONPATH: .
|
|
run: pytest tests -q -rs
|
|
|
|
swisseph-image:
|
|
name: Build obrazu silnika B (swisseph)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Obraz kompiluje pyswisseph ze źródeł (brak wheeli dla cp312), więc ten
|
|
# build jest realnym testem Dockerfile'a — nie tylko pobraniem paczek.
|
|
- name: docker build
|
|
run: docker build -t astrololo/engine-swisseph:ci services/engine-swisseph
|
|
|
|
# Test biegnie WEWNĄTRZ obrazu, bez sieci i bez kontenera w tle. Poprzednia
|
|
# wersja startowała kontener w tle (--name swe) i pukała curl-em w
|
|
# localhost:8003 — co miało dwie wady:
|
|
# 1. nie sprzątała kontenera, więc każdy kolejny przebieg padał na
|
|
# konflikcie nazwy (Conflict. The container name "/swe" is already in use),
|
|
# 2. job Gitea Actions sam działa w kontenerze, a -p publikuje port na
|
|
# HOŚCIE — więc localhost joba to nie ten sam localhost.
|
|
# Wywołanie funkcji endpointów wprost omija oba problemy, a sprawdza to samo:
|
|
# obraz się zbudował, pyswisseph liczy, kontrakt /positions się zgadza.
|
|
# --rm gwarantuje, że nic nie zostaje po przebiegu.
|
|
- name: Smoke test (health + pozycje) wewnątrz obrazu
|
|
run: |
|
|
docker run --rm astrololo/engine-swisseph:ci python - <<'PY'
|
|
from datetime import datetime, timezone
|
|
|
|
from app.main import DEFAULT_OBJECTS, PositionsRequest, health, positions
|
|
|
|
h = health()
|
|
assert h["status"] == "ok", h
|
|
print("health:", h)
|
|
|
|
# Horoskop referencyjny (30.04.1984) — ten sam, na którym opieramy testy
|
|
# silnika własnego; sprawdzamy, że silnik B faktycznie liczy.
|
|
req = PositionsRequest(when_utc=datetime(1984, 4, 30, 9, 20, tzinfo=timezone.utc),
|
|
lat=50.0647, lon=19.9450)
|
|
out = positions(req)
|
|
by = {p["name"]: p for p in out["positions"]}
|
|
|
|
assert out["engine"] == "swisseph", out["engine"]
|
|
assert len(by) == len(DEFAULT_OBJECTS), sorted(by)
|
|
sun = by["Sun"]["longitude"]
|
|
assert 39.5 < sun < 41.0, f"Slonce poza oczekiwanym zakresem: {sun}"
|
|
nn, sn = by["North Node"]["longitude"], by["South Node"]["longitude"]
|
|
assert abs(((sn - nn) % 360.0) - 180.0) < 1e-6, (nn, sn)
|
|
|
|
print(f"Sun={sun:.4f} NN={nn:.4f} obiektow={len(by)}")
|
|
|
|
# /houses — kontrakt parzystosci (LOG-28) po stronie DOMOW. Sprawdzamy
|
|
# nie tylko, ze liczy, ale i ze ODMAWIA tam, gdzie system nie istnieje:
|
|
# ciche podstawienie innego systemu byloby niewykrywalne dla wolajacego.
|
|
from fastapi import HTTPException
|
|
|
|
from app.main import _HOUSE_CODES, HousesRequest, houses
|
|
|
|
WHEN = "1984-04-30T09:20:00Z"
|
|
h = houses(HousesRequest(when_utc=WHEN, lat=50.0647, lon=19.9450, system="campanus"))
|
|
assert h["engine"] == "swisseph" and h["system"] == "campanus"
|
|
assert len(h["cusps"]) == 12, h["cusps"]
|
|
assert [c["house"] for c in h["cusps"]] == list(range(1, 13))
|
|
assert all(0.0 <= c["longitude"] < 360.0 for c in h["cusps"])
|
|
assert {"Asc", "MC", "ARMC"} <= set(h["angles"]), h["angles"]
|
|
|
|
# kazdy ogloszony system musi dac 12 cuspow tam, gdzie ma definicje
|
|
for name in _HOUSE_CODES:
|
|
out = houses(HousesRequest(when_utc=WHEN, lat=50.0647, lon=19.9450, system=name))
|
|
assert len(out["cusps"]) == 12, name
|
|
print(f"/houses: {len(_HOUSE_CODES)} systemow OK")
|
|
|
|
for bad, why in ((dict(lat=69.65, lon=18.96, system="placidus"), "Tromso/placidus"),
|
|
(dict(lat=50.0, lon=19.0, system="nie-ma-takiego"), "nieznany system")):
|
|
try:
|
|
houses(HousesRequest(when_utc=WHEN, **bad))
|
|
raise AssertionError(f"{why}: mialo byc 422, a przeszlo")
|
|
except HTTPException as e:
|
|
assert e.status_code == 422, (why, e.status_code)
|
|
print("/houses: odmowy poza dziedzina OK")
|
|
|
|
print("SMOKE OK")
|
|
PY
|
|
|
|
# Zgodność naszych domów z wyrocznią (Swiss Ephemeris). Odpalamy WEWNĄTRZ
|
|
# obrazu silnika B — tylko tam jest pyswisseph — montując naszą warstwę
|
|
# logiczną i framework. Bez klastra i bez HTTP: to czysta funkcja.
|
|
# BLOKUJE build: błędny system domów jest CICHY (wykres wygląda dobrze,
|
|
# planety siedzą w złych domach), więc lepiej zatrzymać go przed wypuszczeniem
|
|
# niż wykryć po fakcie.
|
|
- name: Domy — zgodność z wyrocznią (brzegi + wnętrze)
|
|
run: |
|
|
# Pliki WBUDOWUJEMY w obraz, a nie montujemy przez -v. Powód ten sam,
|
|
# dla którego wyżej nie startujemy kontenera w tle: job Gitea Actions sam
|
|
# działa w kontenerze, więc `-v $PWD/...` docker rozwiązuje na HOŚCIE,
|
|
# gdzie tej ścieżki nie ma. Docker nie zgłasza wtedy błędu — po cichu
|
|
# tworzy PUSTY katalog, przez co skrypt „znika". Kontekst builda jest
|
|
# strumieniowany do demona, więc działa niezależnie od tego, gdzie on stoi.
|
|
docker build -t astrololo/oracle:ci -f - . <<'DOCKERFILE'
|
|
FROM astrololo/engine-swisseph:ci
|
|
COPY services/logic /logic
|
|
COPY tests/oracle /oracle
|
|
ENV LOGIC_PATH=/logic
|
|
DOCKERFILE
|
|
docker run --rm astrololo/oracle:ci python /oracle/run.py --mode build
|
|
|
|
# Obraz pomocniczy nie jest już potrzebny — a runner miał już incydent
|
|
# „no space left on device", więc sprzątamy po sobie od razu.
|
|
- name: Usuń obraz pomocniczy wyroczni
|
|
if: always()
|
|
run: docker rmi -f astrololo/oracle:ci 2>/dev/null || true
|
|
|
|
# Sprzątanie po POPRZEDNICH przebiegach starej wersji workflow, która
|
|
# zostawiała kontener „swe" na runnerze i blokowała nazwę. Nowa wersja
|
|
# kontenera w tle nie tworzy, więc to tylko jednorazowe uprzątnięcie.
|
|
- name: Usuń osierocony kontener ze starych przebiegów
|
|
if: always()
|
|
run: docker rm -f swe 2>/dev/null || true
|
|
|
|
compile-all:
|
|
name: Kontrola składni wszystkich warstw
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
# Sam kompilator — bez instalowania zależności warstw (w tym AGPL-owego
|
|
# silnika swisseph, który nie wchodzi do produktu).
|
|
- name: py_compile
|
|
run: python -m compileall -q services
|