From ad0041db7468b59b6d996608d9e9c62fc177d1ba Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 13 May 2026 12:53:49 -0400 Subject: [PATCH] =?UTF-8?q?FT=20flake=20mitigations=20triggered=20by=20pip?= =?UTF-8?q?elines=20#302/#303=20=E2=80=94=20three=20independent=20fixes=20?= =?UTF-8?q?consolidated=20in=20one=20commit;=20test=5Fadmin=5Ftarot.=5Flog?= =?UTF-8?q?in=5Fto=5Fadmin=20now=20waits=20on=20`"Site=20administration"`?= =?UTF-8?q?=20body=20substring=20before=20returning,=20same=20shape=20as?= =?UTF-8?q?=20054b0aa's=20test=5Fadmin.py=20fix=20=E2=80=94=20the=20helper?= =?UTF-8?q?=20used=20to=20click=20submit=20+=20return=20immediately,=20let?= =?UTF-8?q?ting=20the=20three=20TarotAdminTest=20tests=20race=20their=20su?= =?UTF-8?q?bsequent=20`browser.get(/admin/epic/tarotcard/)`=20against=20th?= =?UTF-8?q?e=20in-flight=20POST=20=E2=86=92=20302=20=E2=86=92=20admin=20ho?= =?UTF-8?q?me=20navigation=20so=20on=20a=20slow=20CI=20runner=20the=20new?= =?UTF-8?q?=20GET=20cancelled=20the=20unfinished=20POST,=20the=20session?= =?UTF-8?q?=20cookie=20was=20never=20set,=20the=20browser=20landed=20back?= =?UTF-8?q?=20on=20`/admin/login/=3Fnext=3D=E2=80=A6`,=20and=20the=20downs?= =?UTF-8?q?tream=20assertion=20saw=20the=20login-page=20body=20(`'Earthman?= =?UTF-8?q?=20Deck'=20not=20found=20in=20'Django=20administration\nEmail:\?= =?UTF-8?q?nPassword:'`=20in=20#303,=20plus=20a=20NoSuchElementException?= =?UTF-8?q?=20for=20"The=20Schiz"=20on=20test=5Fadmin=5Fearthman=5Fcard=5F?= =?UTF-8?q?detail's=20link-text=20click)=20=E2=80=94=20wait=5Ffor=20w.=20a?= =?UTF-8?q?ssertIn=20retries=20til=20the=20post-login=20page=20actually=20?= =?UTF-8?q?renders=20so=20the=20rest=20of=20the=20helper's=20callers=20sta?= =?UTF-8?q?rt=20from=20a=20settled=20state=20;=20test=5Fjasmine=20swaps=20?= =?UTF-8?q?`wait=5Ffor(check=5Fresults)`=20=E2=86=92=20`wait=5Ffor=5Fslow(?= =?UTF-8?q?check=5Fresults,=20timeout=3D60)`=20=E2=80=94=20the=20spec=20su?= =?UTF-8?q?ite=20has=20grown=20well=20past=20the=2010s=20MAX=5FWAIT=20the?= =?UTF-8?q?=20default=20`wait=5Ffor`=20decorator=20affords=20+=20under=20C?= =?UTF-8?q?I=20contention=20(parallel=20Selenium=20workers=20competing=20f?= =?UTF-8?q?or=20CPU=20on=20the=20same=20droplet)=20Jasmine's=20`.jasmine-o?= =?UTF-8?q?verall-result`=20was=20still=20reporting=20`"Running..."`=20at?= =?UTF-8?q?=2010s=20when=20the=20assertion=20fired=20w.=20`(no=20detail)`?= =?UTF-8?q?=20failure=20list=20(#303);=20check=5Fresults=20body=20is=20unc?= =?UTF-8?q?hanged=20=E2=80=94=20`"Running..."`=20doesn't=20match=20the=20`?= =?UTF-8?q?0=20failures`=20regex=20so=20it=20falls=20into=20the=20failure?= =?UTF-8?q?=20branch=20+=20raises=20AssertionError,=20which=20wait=5Ffor?= =?UTF-8?q?=5Fslow=20naturally=20retries=20til=20the=20result=20settles=20?= =?UTF-8?q?or=2060s=20elapses=20;=20base.=5Fmake=5Fbrowser=20wraps=20`webd?= =?UTF-8?q?river.Firefox(options=3Doptions)`=20in=20try/except=20WebDriver?= =?UTF-8?q?Exception=20w.=20one=20sleep+retry=20when=20`"geckodriver"`=20a?= =?UTF-8?q?ppears=20in=20the=20error=20message=20=E2=80=94=20covers=20the?= =?UTF-8?q?=20spawn=20race=20under=20--parallel=20where=20multiple=20worke?= =?UTF-8?q?rs=20hit=20the=20same=20binary=20mid-permission-set=20+=20one?= =?UTF-8?q?=20of=20them=20gets=20`'geckodriver'=20executable=20may=20have?= =?UTF-8?q?=20wrong=20permissions`=20(1=20test=20in=20#302=20+=201=20test?= =?UTF-8?q?=20in=20#303,=20different=20test=20classes=20each=20time,=20con?= =?UTF-8?q?firming=20infra=20not=20test-logic);=20narrow=20filter=20on=20`?= =?UTF-8?q?"geckodriver"`=20so=20a=20genuine=20install=20fault=20still=20f?= =?UTF-8?q?ails=20fast=20=E2=80=94=20both=20attempts=20would=20surface=20t?= =?UTF-8?q?he=20same=20error=20in=20<1s=20;=20deferred=20option=20filed=20?= =?UTF-8?q?to=20memory=20(project=5Fci=5Fremove=5Fpip=5Finstall=5Fdeferred?= =?UTF-8?q?.md)=20=E2=80=94=20dropping=20the=20`pip=20install=20-r=20requi?= =?UTF-8?q?rements.dev.txt`=20line=20from=20each=20FT=20step=20would=20sav?= =?UTF-8?q?e=20~5=20min/pipeline=20(CI=20image=20drifted=20from=20requirem?= =?UTF-8?q?ents.dev.txt=20since=20a21e6aa=20so=20the=20install=20actually?= =?UTF-8?q?=20downloads=2030+=20packages=20every=20step=20instead=20of=20t?= =?UTF-8?q?he=20intended=20"already=20satisfied"=20no-op=20verify)=20but?= =?UTF-8?q?=20loses=20the=20dep-drift=20safety=20net;=20declined=20for=20n?= =?UTF-8?q?ow,=20revisit=20when=20wall-clock=20pain=20>=20safety=20value?= =?UTF-8?q?=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Opus 4.7 --- src/functional_tests/base.py | 12 +++++++++++- src/functional_tests/test_admin_tarot.py | 8 ++++++++ src/functional_tests/test_jasmine.py | 9 ++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/functional_tests/base.py b/src/functional_tests/base.py index b587d79..7d950a4 100644 --- a/src/functional_tests/base.py +++ b/src/functional_tests/base.py @@ -42,7 +42,17 @@ class FunctionalTest(StaticLiveServerTestCase): options = webdriver.FirefoxOptions() if os.environ.get("HEADLESS"): options.add_argument("--headless") - browser = webdriver.Firefox(options=options) + try: + browser = webdriver.Firefox(options=options) + except WebDriverException as e: + # Geckodriver spawn race under --parallel: another worker may be + # mid-spawn and the binary briefly reports "wrong permissions". + # One sleep+retry absorbs the race; a genuine install fault will + # fail both attempts. See pipeline #302/#303. + if "geckodriver" not in str(e): + raise + time.sleep(1) + browser = webdriver.Firefox(options=options) browser.set_window_size(width, height) return browser diff --git a/src/functional_tests/test_admin_tarot.py b/src/functional_tests/test_admin_tarot.py index 9dd3e37..b7e1d1b 100644 --- a/src/functional_tests/test_admin_tarot.py +++ b/src/functional_tests/test_admin_tarot.py @@ -55,6 +55,14 @@ class TarotAdminTest(FunctionalTest): self.browser.find_element(By.ID, "id_username").send_keys("admin@example.com") self.browser.find_element(By.ID, "id_password").send_keys("correct-password") self.browser.find_element(By.CSS_SELECTOR, "input[type=submit]").click() + # Wait for the POST → 302 → admin home to actually land before + # returning — otherwise the caller's `browser.get(...)` can race + # the in-flight form submit, hit /admin/login/?next=... and never + # recover. Same flake class as 054b0aa fixed in test_admin.py. + self.wait_for(lambda: self.assertIn( + "Site administration", + self.browser.find_element(By.TAG_NAME, "body").text, + )) # ------------------------------------------------------------------ # # Test 1a — admin home lists Tarot cards + Deck variants under Epic # diff --git a/src/functional_tests/test_jasmine.py b/src/functional_tests/test_jasmine.py index 43bc406..cf4d9e0 100644 --- a/src/functional_tests/test_jasmine.py +++ b/src/functional_tests/test_jasmine.py @@ -20,4 +20,11 @@ class JasmineTest(FunctionalTest): detail = "\n".join(f.text for f in failures) if failures else "(no detail)" self.fail(f"{result.text}\nFailing specs:\n{detail}") - self.wait_for(check_results) + # wait_for_slow w. 60s ceiling — the spec suite has grown well past the + # 10s MAX_WAIT the default wait_for affords. Under CI contention + # (parallel Selenium workers competing for CPU) Jasmine's still + # reporting "Running..." at 10s; pipeline #303 caught this. The + # check_results body keeps raising AssertionError ("Running..." doesn't + # match the 0-failures regex) so wait_for_slow naturally retries til + # the result settles or the timeout fires. + self.wait_for_slow(check_results, timeout=60)