Skip to content

fix(testy): rozstrzyganie szablonu nie może wymagać bazy w teście bez django_db - #770

Merged
mpasternak merged 1 commit into
devfrom
fix-authserver-template
Aug 16, 2026
Merged

fix(testy): rozstrzyganie szablonu nie może wymagać bazy w teście bez django_db#770
mpasternak merged 1 commit into
devfrom
fix-authserver-template

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Closes #766

Mechanizm

Winowajcą nie jest ani cached.Loader, ani TEMPLATES[0]["DIRS"], ani żadne
przeciekające override_settings. Silnik szablonów przez cały przebieg ma
poprawne dirs=['…/src/django_bpp/templates']. Problemem jest loader
dbtemplates
.

BPP stawia dbtemplates.loader.Loader przed loaderami dyskowymi
(settings/local.py:108), żeby dowolny szablon dało się nadpisać wierszem
w django_template. Zanim ten loader odpowie „nie mam, następny", pyta
should_skip()known_names(), a to przy zimnym cache wykonuje
SELECT name FROM django_template. Innymi słowy: w tej konfiguracji
get_template("cokolwiek.html") potrafi sięgnąć po bazę.

Cache known_names() to zmienna globalna procesu
(dbtemplates.utils.names._names), kasowana sygnałem post_save/post_delete
przy każdym zapisie i usunięciu wiersza Template
(dbtemplates/models.py:85-86). Skutek: czy test bez django_db
renderujący szablon przejdzie, zależy wyłącznie od tego, co wcześniej
przeleciało na tym samym workerze xdist.

Prawdziwy wyjątek na dole traceback'u (przechwycony sondą w pełnym przebiegu)
to nie TemplateDoesNotExist, tylko:

dbtemplates/loader.py:29:  if should_skip(template_name):
dbtemplates/utils/names.py:90:  names = known_names()
dbtemplates/utils/names.py:77:  _names = _load()
dbtemplates/utils/names.py:45:  return frozenset(Template.objects.values_list("name", flat=True))
…
django/db/backends/base/base.py:296: RuntimeError:
    Database access not allowed, use the "django_db" mark, …

czyli SELECT "django_template"."name" … FROM "django_template" wykonany
z wnętrza get_template() w teście, który bazy nie zamówił.

Dlaczego to wyglądało na „tylko w pełnym przebiegu"

  • W izolacji pliku 18/18 przechodzi, bo test_auth_server.py zaczyna się
    od testów @pytest.mark.django_db (test_is_superuser_*), które ocieplają
    cache nazw — dwa późniejsze testy bez django_db już nie muszą pytać bazy.

  • Uruchomione pojedynczo padają od zawsze (0,6 s, bez żadnych zmian
    w kodzie) — to jest deterministyczne repro, którego brakowało w zgłoszeniu:

    uv run pytest "src/django_bpp/tests/test_auth_server.py::test_szablon_logowania_nie_udaje_awarii"
    
  • CI tego nie łapało, bo przy 12 shardach te dwa testy trafiały do procesu,
    w którym cache był ciepły (albo nie było w nim testu zapisującego wiersz
    Template, który go kasuje). Sharding nie „maskuje błędu" przypadkiem —
    zmienia dokładnie tę zmienną, od której defekt zależy: skład testów
    w jednym procesie.

  • -p no:randomly nic nie zmienia, bo to nie jest kwestia losowej kolejności,
    tylko tego, które testy dzielą proces.

Problem był znacznie szerszy niż te dwa testy: sonda wpięta w teardown każdego
testu (próbny get_template("auth_server/login.html")) pokazała ~2350
testów bez django_db na samym workerze gw0
, które przy zimnym cache
zachowałyby się identycznie. Przechodziły dotąd wyłącznie na szczęście.

Co zmieniono

src/conftest.py (obie połówki opisane w komentarzu przy kodzie):

  1. known_names() przy bazie zablokowanej przez pytest-django zwraca pusty
    zbiór
    zamiast wybuchać. To nie jest obejście, tylko stwierdzenie faktu:
    test bez django_db nie ma bazy, więc nie ma też żadnych nadpisań z bazy —
    loader ma oddać sterowanie loaderom dyskowym. Łapiemy wyłącznie
    RuntimeError blokady (rozpoznawany po treści komunikatu), każdy inny
    RuntimeError leci dalej.
  2. Cache nazw jest zerowany przed każdym testem, żeby żaden nie dziedziczył
    po poprzedniku nazw z bazy wycofanej rollbackiem. To druga, cichsza wersja
    tego samego sprzężenia: ciepły, nieaktualny zbiór potrafi kazać loaderowi
    iść do bazy po nazwę, której już nie ma — albo pominąć nadpisanie, które
    w bieżącym teście naprawdę istnieje.

Produkcja nie jest ruszona: tam baza zawsze jest, a zachowanie loadera się nie
zmienia. Do dwóch testów z test_auth_server.py celowo nie dołożono
@pytest.mark.django_db — sedno poprawki polega na tym, że nie powinny go
potrzebować, a marker naprawiłby tylko te dwa przypadki z ~2350.

Test regresji

src/bpp/tests/test_dbtemplates_bez_bazy.py (4 testy):

  • test_szablon_z_dysku_rozstrzyga_sie_przy_zimnym_cache_i_bez_bazy — właściwy
    strażnik: jawnie wywołuje invalidate_known_names() (czyli odtwarza stan po
    zapisie wiersza Template) i renderuje szablon z dysku bez django_db.
    Deterministyczny, niezależny od kolejności testów.
  • test_z_baza_nadpisanie_szablonu_wierszem_w_bazie_dalej_dziala — druga strona
    medalu: guard nie może uciszyć dbtemplates tam, gdzie baza JEST dostępna.
  • test_loader_dbtemplates_pyta_o_znane_nazwy — pilnuje
    DBTEMPLATES_SKIP_UNKNOWN_NAMES = True, bo to na tej fladze guard się opiera.
  • test_cache_nazw_jest_zimny_na_starcie_testu — kontrakt fixture'u zerującego.

Weryfikacja w obie strony (jawnie, na tym samym venvie i kontenerach)

Bez poprawki (git stash push src/conftest.py):

FAILED src/bpp/tests/test_dbtemplates_bez_bazy.py::test_cache_nazw_jest_zimny_na_starcie_testu
FAILED src/bpp/tests/test_dbtemplates_bez_bazy.py::test_szablon_z_dysku_rozstrzyga_sie_przy_zimnym_cache_i_bez_bazy
FAILED src/django_bpp/tests/test_auth_server.py::test_szablon_logowania_nie_udaje_awarii
FAILED src/django_bpp/tests/test_auth_server.py::test_szablony_authservera_maja_polskie_znaki
4 failed, 2 passed in 3.21s

Z poprawką (ten sam zestaw + cały test_auth_server.py):

22 passed in 3.40s

Plus deterministyczne repro pojedynczego testu: przed poprawką 1 failed
w 0,63 s, po poprawce 1 passed w 0,53 s.

Pełna suita

Baseline na czystym dev (odtworzony lokalnie, świeże kontenery,
-n 4 -m "not playwright" -p no:randomly -q):

2 failed, 9391 passed, 4 skipped, 1 xfailed w 361,79 s
FAILED …::test_szablon_logowania_nie_udaje_awarii
FAILED …::test_szablony_authservera_maja_polskie_znaki

Po poprawce (uv run pytest -n 4 -m "not playwright" -q, świeże kontenery,
losowa kolejność):

9397 passed, 4 skipped, 1 xfailed w 319,90 s   (exit 0)

Arytmetyka się zgadza: baseline to 9393 testy (2 failed + 9391 passed),
po poprawce 9397 = 9393 + 4 nowe testy regresji.

Newsfragment: src/bpp/newsfragments/766.bugfix.rst.

Dwa testy z django_bpp/tests/test_auth_server.py
(test_szablon_logowania_nie_udaje_awarii,
test_szablony_authservera_maja_polskie_znaki) padaly w pelnym przebiegu
suity, a przechodzily w izolacji i na shardowanym CI. Winowajca nie jest
ani cached.Loader, ani DIRS, ani override_settings — tylko loader
dbtemplates.

BPP stawia dbtemplates.loader.Loader PRZED loaderami dyskowymi, zeby
dowolny szablon dalo sie nadpisac wierszem w django_template. Zanim ten
loader odpowie "nie mam, nastepny", pyta should_skip() -> known_names(),
a to przy zimnym cache robi SELECT name FROM django_template. Czyli
get_template("cokolwiek.html") potrafi siegnac po baze.

Cache known_names() to zmienna globalna PROCESU
(dbtemplates.utils.names._names), kasowana sygnalem przy kazdym zapisie
i usunieciu wiersza Template. Efekt: czy test bez django_db renderujacy
szablon przejdzie, zalezy wylacznie od tego, co wczesniej przelecialo na
tym samym workerze xdist. W izolacji te dwa testy poprzedzaly testy
@pytest.mark.django_db z tego samego pliku, ktore ocieplaly cache; na CI
podzial na 12 shardow inaczej rozkladal testy po procesach. Uruchomione
pojedynczo padaly od zawsze:

  pytest "src/django_bpp/tests/test_auth_server.py::test_szablon_logowania_nie_udaje_awarii"

Poprawka jest w src/conftest.py i usuwa zaleznosc, nie objaw:

- known_names() przy zablokowanej przez pytest-django bazie zwraca pusty
  zbior zamiast wybuchac. To nie obejscie, tylko fakt: test bez django_db
  nie ma bazy, wiec nie ma tez zadnych nadpisan z bazy — loader ma oddac
  sterowanie loaderom dyskowym. Lapiemy WYLACZNIE RuntimeError blokady
  (po tresci komunikatu), kazdy inny leci dalej.
- cache nazw jest zerowany przed kazdym testem, zeby zaden nie dziedziczyl
  po poprzedniku nazw z bazy wycofanej rollbackiem (druga, cichsza wersja
  tego samego sprzezenia: nadpisanie z bazy moglo zostac pominiete).

Dzieki temu chronione sa wszystkie testy renderujace szablony bez
django_db, a nie tylko te dwa — mierzone sonda w pelnym przebiegu: na
samym gw0 bylo ich ~2350.

Closes #766

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mpasternak
mpasternak merged commit fdd72e6 into dev Aug 16, 2026
22 checks passed
@mpasternak
mpasternak deleted the fix-authserver-template branch August 16, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dwa testy test_auth_server.py padają na dev: TemplateDoesNotExist dla auth_server/login.html

1 participant