diff --git a/tests/studio/install/test_rocm_rdna_routing.py b/tests/studio/install/test_rocm_rdna_routing.py index b4aeafb7e4..d5a1be74ea 100644 --- a/tests/studio/install/test_rocm_rdna_routing.py +++ b/tests/studio/install/test_rocm_rdna_routing.py @@ -12,6 +12,7 @@ at import) resolves from a clean process. from __future__ import annotations import json +import os import subprocess import sys from pathlib import Path @@ -43,6 +44,15 @@ _ARCHES = { _CHILD = """ import json, sys sys.path.insert(0, {tests!r}) +# Import bitsandbytes under the real torch first. unsloth_zoo pulls it in, and it +# picks a compute backend at import: once the spoof reports an AMD GPU, it loads +# its ROCm/CUDA ops, which a CPU-only torch cannot satisfy (no libhipblas, no +# torch._C._cuda_getCurrentRawStream) and the child dies before printing RESULT. +# Nothing here tests bitsandbytes, so let it see the honest hardware. +try: + import bitsandbytes # noqa: F401 +except Exception: + pass import _zoo_rocm_spoof as spoof arches = {arches!r} spoof.apply(arches[0]) @@ -60,7 +70,11 @@ print("RESULT " + json.dumps({{"device_type": device_type, "targets": targets}}) @pytest.fixture(scope = "module") def routed(): code = _CHILD.format(tests = str(_TESTS_DIR), arches = list(_ARCHES)) - proc = subprocess.run([sys.executable, "-c", code], capture_output = True, text = True) + # get_device_type() returns "mlx" before it ever looks at torch on Darwin arm64 + # with mlx installed, so the spoof would be ignored. Force the GPU path to keep + # the assertion live there instead of skipping it. + env = {**os.environ, "UNSLOTH_FORCE_GPU_PATH": "1"} + proc = subprocess.run([sys.executable, "-c", code], capture_output = True, text = True, env = env) line = next((l for l in proc.stdout.splitlines() if l.startswith("RESULT ")), None) assert line, f"child produced no result.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" return json.loads(line[len("RESULT ") :]) diff --git a/tests/studio/playwright_ui_font_scale.py b/tests/studio/playwright_ui_font_scale.py index 903d7745c1..9595ea3adc 100644 --- a/tests/studio/playwright_ui_font_scale.py +++ b/tests/studio/playwright_ui_font_scale.py @@ -16,6 +16,7 @@ import os import sys from pathlib import Path +from playwright.sync_api import TimeoutError as PWTimeout from playwright.sync_api import sync_playwright sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -46,6 +47,18 @@ def near( return a is not None and b is not None and abs(a - b) <= tol +_VP = 'document.querySelector("[data-radix-select-viewport]")' +SCROLL_TOP_JS = f"() => {_VP}.scrollTop" +SCROLLABLE_JS = f"() => {{ const vp = {_VP}; return !!vp && vp.scrollHeight > vp.clientHeight; }}" +VIEWPORT_STATE_JS = f""" +() => {{ + const vp = {_VP}; + return vp + ? {{ scrollHeight: vp.scrollHeight, clientHeight: vp.clientHeight, top: vp.scrollTop }} + : null; +}} +""" + MEASURE_JS = """ () => { const fs = (el) => (el ? parseFloat(getComputedStyle(el).fontSize) : null); @@ -83,15 +96,22 @@ def set_input(page, label, value): def open_appearance(page): - page.keyboard.press("Control+,") - page.wait_for_timeout(700) - if page.get_by_role("dialog").count() == 0: - page.keyboard.press("Meta+,") - page.wait_for_timeout(700) - if page.get_by_role("dialog").count() == 0: - fail("settings dialog did not open") - page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click() - page.wait_for_timeout(600) + # The shortcut can fire before the app has wired its key handler, so press + # each chord once behind a fixed sleep and a slow boot loses the dialog. + # Alternate them on a bounded retry, waiting on the dialog itself. + dialog = page.get_by_role("dialog") + for attempt in range(10): + page.keyboard.press("Meta+," if attempt % 2 else "Control+,") + try: + dialog.first.wait_for(state = "visible", timeout = 2_000) + break + except PWTimeout: + continue + if dialog.count() == 0: + fail("settings dialog did not open after 10 attempts") + dialog.get_by_role("button").filter(has_text = "Appearance").first.click() + # Wait for the control the caller is about to drive, not a fixed interval. + page.locator("input[aria-label='UI font size']").wait_for(state = "visible", timeout = 15_000) def main(): @@ -155,39 +175,46 @@ def main(): page.wait_for_timeout(400) step("overflowing select scrolls its Radix viewport") - page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click() - page.wait_for_timeout(600) + voice = page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first + voice.click() page.set_viewport_size({"width": 1440, "height": 480}) - page.locator("[aria-label='Dictation language']").click() - page.wait_for_timeout(700) - state = page.evaluate( - """ - () => { - const vp = document.querySelector("[data-radix-select-viewport]"); - return vp - ? { scrollable: vp.scrollHeight > vp.clientHeight, top: vp.scrollTop } - : null; - } - """ - ) - if not state or not state["scrollable"]: - fail(f"select viewport not scrollable: {state}") - for _ in range(6): + trigger = page.locator("[aria-label='Dictation language']") + trigger.wait_for(state = "visible") + trigger.click() + + viewport = page.locator("[data-radix-select-viewport]") + viewport.wait_for(state = "visible") + # Wait for the overflow itself rather than a fixed sleep: the list is + # populated asynchronously, so measuring too early reads it as short. + try: + page.wait_for_function(SCROLLABLE_JS, timeout = 10_000) + except PWTimeout: + fail(f"select viewport not scrollable: {page.evaluate(VIEWPORT_STATE_JS)}") + + # Radix moves focus into the listbox after the content opens, so a fixed + # burst of presses can land on the trigger and scroll nothing. Press until + # it moves instead; a real regression still fails, just after more tries. + kb_top = 0 + for _ in range(40): page.keyboard.press("ArrowDown") - page.wait_for_timeout(100) - kb_top = page.evaluate( - "() => document.querySelector('[data-radix-select-viewport]').scrollTop" - ) + kb_top = page.evaluate(SCROLL_TOP_JS) + if kb_top > 0: + break + page.wait_for_timeout(50) if not kb_top > 0: - fail(f"keyboard did not scroll the select viewport: {kb_top}") - vp_box = page.locator("[data-radix-select-viewport]").bounding_box() + fail(f"keyboard did not scroll the select viewport after 40 presses: {kb_top}") + + vp_box = viewport.bounding_box() page.mouse.move(vp_box["x"] + vp_box["width"] / 2, vp_box["y"] + 40) page.mouse.wheel(0, -400) - page.wait_for_timeout(300) - wheel_top = page.evaluate( - "() => document.querySelector('[data-radix-select-viewport]').scrollTop" - ) - if not wheel_top < kb_top: + try: + page.wait_for_function( + "top => document.querySelector('[data-radix-select-viewport]').scrollTop < top", + arg = kb_top, + timeout = 10_000, + ) + except PWTimeout: + wheel_top = page.evaluate(SCROLL_TOP_JS) fail(f"wheel did not scroll the select viewport: {kb_top} -> {wheel_top}") page.keyboard.press("Escape") page.set_viewport_size({"width": 1440, "height": 900})