Compare commits

...

2 Commits

Author SHA1 Message Date
Disco DeDisco
af1a90e76b woodpecker main.yaml: A+B combined CI dependency speedup — test-UTs-n-ITs swaps image from python:3.13-slim to gitea.earthmanrpg.me/discoman/python-tdd-ci:latest (same image as the two FT steps) so all 3 steps inherit the requirements.dev.txt deps Dockerfile.ci already pre-installs; all 3 steps switch pip install -r requirements.txtpip install -r requirements.dev.txt so the install collapses to ~5-10s of "already satisfied" verification per step (vs ~30-60s of unpinned PyPI resolver+download against requirements.txt); ~3-5 min saved per pipeline run; drift safety net preserved — pip still installs deltas if requirements.dev.txt is ahead of the image, so a stale image doesn't break CI, it just runs slower until the image is rebuilt + pushed
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
Code architected by Disco DeDisco <discodedisco@outlook.com>

Git commit message Co-Authored-By:

Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:36:04 -04:00
Disco DeDisco
8240de6b45 functional_tests/room_page.py: extract shared FT helpers into a dedicated module so renaming a test_X.py file doesn't cascade-break sibling imports — _fill_room_via_orm (was in test_room_role_select, imported by test_room_tray + test_deck_contribution + test_game_invite + test_room_gatekeeper + test_room_sig_select), _assign_all_roles (was in test_room_sig_select, imported by test_room_tray + test_deck_contribution), and _equip_earthman_deck (duplicated verbatim in test_room_role_select + test_component_tray_tooltip — the test_component_tray_tooltip copy of _fill_room_via_orm was also a near-duplicate, missing only the gamers-return both call sites discarded anyway); SIG_SEAT_ORDER constant moves along since _assign_all_roles depends on it; mirrors the existing post_page.py pattern; underscored helper names kept so the "test infrastructure, not API" signal survives the relocation; dropped now-unused per-file imports (DeckVariant from test_room_role_select; Note/DeckVariant/TableSeat/TarotCard from test_room_sig_select; GateSlot from test_component_tray_tooltip); regression gate: GatekeeperTest (8 FTs) green via the new helper home; smoke-imports green across all 8 touched modules
Code architected by Disco DeDisco <discodedisco@outlook.com>

Git commit message Co-Authored-By:

Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:23:08 -04:00
9 changed files with 135 additions and 130 deletions

View File

@@ -11,19 +11,21 @@ services:
steps:
- name: test-UTs-n-ITs
image: python:3.13-slim
image: gitea.earthmanrpg.me/discoman/python-tdd-ci:latest
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres/python_tdd_test
CELERY_BROKER_URL: redis://redis:6379/0
REDIS_URL: redis://redis:6379/1
# Workspace-shared pip cache — Woodpecker mounts the workspace across
# all steps in a run, so the wheels populated here are reused by
# test-two-browser-FTs + test-FTs (saves ~30-60s × 2 = ~1-2 min per
# run). Cache lives only inside one run; cross-run caching would
# need a volume plugin.
PIP_CACHE_DIR: .pip-cache
commands:
- pip install -r requirements.txt
# `requirements.dev.txt` is the pinned superset Dockerfile.ci pre-
# installs; pinning here means pip skips resolver+download and just
# verifies "already satisfied" (~5-10s) instead of resolving unpinned
# requirements.txt against PyPI from scratch (~30-60s). Drift safety
# net: if requirements.dev.txt has changed since the CI image was
# last rebuilt + pushed, pip installs the delta — slower for that
# run but never broken. See TDD SKILL.md § CI dependency discipline.
- pip install -r requirements.dev.txt
- cd ./src
- python manage.py test apps
when:
@@ -45,7 +47,7 @@ steps:
from_secret: stripe_publishable_key
PIP_CACHE_DIR: .pip-cache
commands:
- pip install -r requirements.txt
- pip install -r requirements.dev.txt
- cd ./src
- python manage.py collectstatic --noinput
- python manage.py test functional_tests --tag=two-browser
@@ -70,7 +72,7 @@ steps:
from_secret: stripe_publishable_key
PIP_CACHE_DIR: .pip-cache
commands:
- pip install -r requirements.txt
- pip install -r requirements.dev.txt
- cd ./src
- python manage.py collectstatic --noinput
- python manage.py test functional_tests --parallel --exclude-tag=channels --exclude-tag=two-browser

View File

@@ -0,0 +1,112 @@
"""Room-page FT helpers — extracted from the per-page FT modules so the
helpers can be imported without coupling consumer FTs to a sibling test
file's lifecycle (renames, deletions). Mirrors the post_page.py pattern.
Naming: underscored to signal "test infrastructure, not API surface", but
they're public within `functional_tests/` — direct imports are fine.
Consumers:
test_room_role_select / test_room_sig_select — define-and-use
test_component_tray_tooltip — _equip_earthman_deck
test_room_tray / test_deck_contribution — both core helpers
test_game_invite / test_room_gatekeeper — _fill_room_via_orm
"""
from django.utils import timezone
from apps.drama.models import Note
from apps.epic.models import (
DeckVariant, GateSlot, Room, TableSeat, TarotCard,
)
from apps.lyric.models import User
SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"]
def _equip_earthman_deck(user):
# get_or_create: TransactionTestCase.flush() wipes migration-seeded
# DeckVariants between tests, so subsequent tests in the same run can't
# find it via filter().
deck, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
)
user.equipped_deck = deck
user.save(update_fields=["equipped_deck"])
def _fill_room_via_orm(room, emails):
"""Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers."""
gamers = []
for i, email in enumerate(emails, start=1):
gamer, _ = User.objects.get_or_create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
gamers.append(gamer)
room.gate_status = Room.OPEN
room.save()
return gamers
def _assign_all_roles(room, role_order=None):
"""Assign roles to all slots, reveal them, and advance to SIG_SELECT.
Also ensures all gamers have an equipped_deck (required for sig_deck_cards)."""
if role_order is None:
role_order = SIG_SEAT_ORDER[:]
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}",
"levity_qualifier": "Elevated",
"gravity_qualifier": "Graven"},
)
# Numbers 01 are the sig deck's Major Arcana (unlocked via Note).
# Seed them with correct Earthman names and qualifiers, then unlock for all gamers.
for number, name, slug in [
(0, "The Nomad", "the-nomad"),
(1, "The Schizo", "the-schizo"),
]:
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=slug,
defaults={"arcana": "MAJOR", "number": number, "name": name,
"levity_qualifier": "Enlightened",
"gravity_qualifier": "Engraven"},
)
for slot in room.gate_slots.order_by("slot_number"):
if slot.gamer:
Note.objects.get_or_create(
user=slot.gamer, slug="super-nomad",
defaults={"earned_at": timezone.now()},
)
Note.objects.get_or_create(
user=slot.gamer, slug="super-schizo",
defaults={"earned_at": timezone.now()},
)
for slot in room.gate_slots.order_by("slot_number"):
if slot.gamer and not slot.gamer.equipped_deck:
slot.gamer.equipped_deck = earthman
slot.gamer.save(update_fields=["equipped_deck"])
TableSeat.objects.update_or_create(
room=room,
slot_number=slot.slot_number,
defaults={
"gamer": slot.gamer,
"role": role_order[slot.slot_number - 1],
"role_revealed": True,
},
)
room.table_status = Room.SIG_SELECT
room.save()

View File

@@ -14,28 +14,11 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat, TarotCard
from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard
from apps.lyric.models import User
def _equip_earthman_deck(user):
deck, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
)
user.equipped_deck = deck
user.save(update_fields=["equipped_deck"])
def _fill_room_via_orm(room, emails):
for i, email in enumerate(emails, start=1):
gamer, _ = User.objects.get_or_create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
from .room_page import _equip_earthman_deck, _fill_room_via_orm
class TrayRoleCardTooltipTest(FunctionalTest):

View File

@@ -22,8 +22,7 @@ from apps.applets.models import Applet
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat
from apps.lyric.models import User
from functional_tests.base import FunctionalTest
from functional_tests.test_room_role_select import _fill_room_via_orm
from functional_tests.test_room_sig_select import _assign_all_roles
from functional_tests.room_page import _fill_room_via_orm
FOUNDER_EMAIL = "founder@test.io"
GAMER_EMAIL = "gamer@test.io"

View File

@@ -47,7 +47,7 @@ except ImportError:
_BILLPOST_READY = False
from apps.lyric.models import User
from functional_tests.base import FunctionalTest
from functional_tests.test_room_role_select import _fill_room_via_orm
from functional_tests.room_page import _fill_room_via_orm
FOUNDER_EMAIL = "founder@test.io"
INVITEE_EMAIL = "invitee@test.io"

View File

@@ -8,7 +8,7 @@ from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, select_token
from apps.lyric.models import Token, User
from .test_room_role_select import _fill_room_via_orm
from .room_page import _fill_room_via_orm
class GatekeeperTest(FunctionalTest):

View File

@@ -8,37 +8,12 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest, ChannelsFunctionalTest
from .management.commands.create_session import create_pre_authenticated_session
from .room_page import _equip_earthman_deck, _fill_room_via_orm
from apps.applets.models import Applet
from apps.epic.models import DeckVariant, Room, GateSlot, TableSeat
from apps.epic.models import Room, GateSlot, TableSeat
from apps.lyric.models import User
def _equip_earthman_deck(user):
# get_or_create: TransactionTestCase.flush() wipes migration-seeded DeckVariants
# between tests, so subsequent tests in the same run can't find it via filter().
deck, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
)
user.equipped_deck = deck
user.save(update_fields=["equipped_deck"])
def _fill_room_via_orm(room, emails):
"""Fill all 6 gate slots and set gate_status=OPEN. Returns list of gamers."""
gamers = []
for i, email in enumerate(emails, start=1):
gamer, _ = User.objects.get_or_create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
gamers.append(gamer)
room.gate_status = Room.OPEN
room.save()
return gamers
class RoleSelectTest(FunctionalTest):
def setUp(self):

View File

@@ -7,13 +7,13 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest, ChannelsFunctionalTest
from .management.commands.create_session import create_pre_authenticated_session
from .room_page import (
SIG_SEAT_ORDER, _assign_all_roles, _fill_room_via_orm,
)
from apps.applets.models import Applet
from apps.drama.models import Note
from apps.epic.models import DeckVariant, Room, TableSeat, TarotCard
from apps.epic.models import Room
from apps.lyric.models import User
from .test_room_role_select import _fill_room_via_orm
# ── Significator Selection ────────────────────────────────────────────────────
#
@@ -23,71 +23,6 @@ from .test_room_role_select import _fill_room_via_orm
#
# ─────────────────────────────────────────────────────────────────────────────
SIG_SEAT_ORDER = ["PC", "NC", "EC", "SC", "AC", "BC"]
def _assign_all_roles(room, role_order=None):
"""Assign roles to all slots, reveal them, and advance to SIG_SELECT.
Also ensures all gamers have an equipped_deck (required for sig_deck_cards)."""
if role_order is None:
role_order = SIG_SEAT_ORDER[:]
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}",
"levity_qualifier": "Elevated",
"gravity_qualifier": "Graven"},
)
# Numbers 01 are the sig deck's Major Arcana (unlocked via Note).
# Seed them with correct Earthman names and qualifiers, then unlock for all gamers.
from django.utils import timezone
for number, name, slug in [
(0, "The Nomad", "the-nomad"),
(1, "The Schizo", "the-schizo"),
]:
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=slug,
defaults={"arcana": "MAJOR", "number": number, "name": name,
"levity_qualifier": "Enlightened",
"gravity_qualifier": "Engraven"},
)
for slot in room.gate_slots.order_by("slot_number"):
if slot.gamer:
Note.objects.get_or_create(
user=slot.gamer, slug="super-nomad",
defaults={"earned_at": timezone.now()},
)
Note.objects.get_or_create(
user=slot.gamer, slug="super-schizo",
defaults={"earned_at": timezone.now()},
)
for slot in room.gate_slots.order_by("slot_number"):
if slot.gamer and not slot.gamer.equipped_deck:
slot.gamer.equipped_deck = earthman
slot.gamer.save(update_fields=["equipped_deck"])
TableSeat.objects.update_or_create(
room=room,
slot_number=slot.slot_number,
defaults={
"gamer": slot.gamer,
"role": role_order[slot.slot_number - 1],
"role_revealed": True,
},
)
room.table_status = Room.SIG_SELECT
room.save()
class SigSelectTest(FunctionalTest):
"""Significator Selection — non-WebSocket tests."""

View File

@@ -5,8 +5,7 @@ from django.test import tag
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from .test_room_role_select import _fill_room_via_orm
from .test_room_sig_select import _assign_all_roles
from .room_page import _assign_all_roles, _fill_room_via_orm
from apps.epic.models import Room
from apps.lyric.models import User