mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-05-14 20:58:00 +00:00
264 lines
9.2 KiB
Python
264 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from urllib.error import URLError
|
|
from urllib.request import urlopen
|
|
|
|
import pytest
|
|
|
|
from quantum_learning.config import project_root
|
|
|
|
try:
|
|
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError: # pragma: no cover - test environment specific
|
|
sync_playwright = None
|
|
PlaywrightTimeoutError = RuntimeError
|
|
|
|
|
|
def _find_free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
try:
|
|
sock.bind(("127.0.0.1", 0))
|
|
except PermissionError:
|
|
pytest.skip("Browser UX tests need localhost ports, which this sandbox blocks.")
|
|
return int(sock.getsockname()[1])
|
|
|
|
|
|
def _wait_for_server(url: str, process: subprocess.Popen[str], timeout_seconds: int = 90) -> None:
|
|
deadline = time.time() + timeout_seconds
|
|
while time.time() < deadline:
|
|
if process.poll() is not None:
|
|
stdout = process.stdout.read() if process.stdout is not None else ""
|
|
raise RuntimeError(f"Jupyter server exited early.\n{stdout}")
|
|
|
|
try:
|
|
with urlopen(url, timeout=2):
|
|
return
|
|
except URLError:
|
|
time.sleep(1)
|
|
|
|
raise TimeoutError(f"Timed out waiting for JupyterLab at {url}")
|
|
|
|
|
|
def _wait_for_notebook(browser_page, notebook_filename: str, heading: str) -> None:
|
|
browser_page.locator(".lm-TabBar-tabLabel", has_text=notebook_filename).last.wait_for(
|
|
timeout=60000
|
|
)
|
|
browser_page.locator(".jp-RenderedHTMLCommon h1", has_text=heading).last.wait_for(
|
|
timeout=60000
|
|
)
|
|
|
|
|
|
def _open_notebook_from_directory(
|
|
browser_page,
|
|
jupyter_lab_server,
|
|
directory: str,
|
|
notebook_filename: str,
|
|
heading: str,
|
|
) -> None:
|
|
url = f"{jupyter_lab_server['base_url']}/lab/tree/{directory}?token={jupyter_lab_server['token']}"
|
|
browser_page.goto(url, wait_until="networkidle")
|
|
browser_page.locator(".jp-DirListing-itemText", has_text=notebook_filename).first.wait_for(
|
|
timeout=60000
|
|
)
|
|
browser_page.locator(".jp-DirListing-itemText", has_text=notebook_filename).first.dblclick()
|
|
_wait_for_notebook(browser_page, notebook_filename, heading)
|
|
|
|
|
|
def _run_all_cells(browser_page) -> None:
|
|
browser_page.locator("text=Run").first.click()
|
|
browser_page.locator("text=Run All Cells").first.click()
|
|
|
|
try:
|
|
browser_page.get_by_role("button", name="Select").click(timeout=5000)
|
|
except PlaywrightTimeoutError:
|
|
pass
|
|
|
|
|
|
def _find_code_editor(browser_page):
|
|
textareas = browser_page.locator("textarea")
|
|
for index in range(textareas.count()):
|
|
candidate = textareas.nth(index)
|
|
if not candidate.is_visible():
|
|
continue
|
|
try:
|
|
value = candidate.input_value()
|
|
except PlaywrightTimeoutError: # pragma: no cover - widget race
|
|
continue
|
|
if "QuantumCircuit" in value:
|
|
return candidate
|
|
raise AssertionError("Could not locate an editable circuit code editor.")
|
|
|
|
|
|
def _wait_for_text_in_body(browser_page, expected_text: str, *, timeout_seconds: int = 120) -> None:
|
|
deadline = time.time() + timeout_seconds
|
|
last_body = ""
|
|
while time.time() < deadline:
|
|
last_body = browser_page.locator("body").inner_text()
|
|
if expected_text in last_body:
|
|
return
|
|
time.sleep(1)
|
|
|
|
raise AssertionError(
|
|
f"Did not find {expected_text!r} in rendered page text.\nLast body text excerpt:\n"
|
|
f"{last_body[-4000:]}"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def jupyter_lab_server():
|
|
if sync_playwright is None:
|
|
pytest.skip("Playwright is not installed in this environment.")
|
|
|
|
root = project_root()
|
|
browser_root = root / ".playwright-browsers"
|
|
if not browser_root.exists():
|
|
pytest.skip("Project-local Playwright browsers are not installed.")
|
|
|
|
port = _find_free_port()
|
|
token = "quantum-learning-browser-test"
|
|
runtime_root = root / ".tmp_test_artifacts" / "browser_jupyter"
|
|
config_dir = runtime_root / "config"
|
|
data_dir = runtime_root / "data"
|
|
runtime_dir = runtime_root / "runtime"
|
|
ipython_dir = runtime_root / "ipython"
|
|
mpl_dir = runtime_root / "matplotlib"
|
|
for path in (config_dir, data_dir, runtime_dir, ipython_dir, mpl_dir):
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"PATH": f"{root / '.venv' / 'bin'}:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
"JUPYTER_CONFIG_DIR": str(config_dir),
|
|
"JUPYTER_DATA_DIR": str(data_dir),
|
|
"JUPYTER_RUNTIME_DIR": str(runtime_dir),
|
|
"IPYTHONDIR": str(ipython_dir),
|
|
"MPLCONFIGDIR": str(mpl_dir),
|
|
"PLAYWRIGHT_BROWSERS_PATH": str(browser_root),
|
|
}
|
|
)
|
|
|
|
command = [
|
|
str(root / ".venv" / "bin" / "jupyter"),
|
|
"lab",
|
|
"--no-browser",
|
|
"--ip=127.0.0.1",
|
|
f"--port={port}",
|
|
f"--IdentityProvider.token={token}",
|
|
f"--ServerApp.root_dir={root}",
|
|
]
|
|
process = subprocess.Popen(
|
|
command,
|
|
cwd=root,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
base_url = f"http://127.0.0.1:{port}"
|
|
_wait_for_server(f"{base_url}/lab?token={token}", process)
|
|
|
|
try:
|
|
yield {"base_url": base_url, "token": token, "browser_root": browser_root}
|
|
finally:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=10)
|
|
except subprocess.TimeoutExpired: # pragma: no cover - cleanup path
|
|
process.kill()
|
|
|
|
|
|
@pytest.fixture()
|
|
def browser_page(jupyter_lab_server):
|
|
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(jupyter_lab_server["browser_root"])
|
|
with sync_playwright() as playwright:
|
|
browser = playwright.chromium.launch(headless=True)
|
|
page = browser.new_page()
|
|
try:
|
|
yield page
|
|
finally:
|
|
browser.close()
|
|
|
|
|
|
@pytest.mark.browser
|
|
def test_course_blueprint_loads_in_jupyterlab(browser_page, jupyter_lab_server):
|
|
url = (
|
|
f"{jupyter_lab_server['base_url']}/lab/tree/notebooks/COURSE_BLUEPRINT.ipynb"
|
|
f"?token={jupyter_lab_server['token']}"
|
|
)
|
|
browser_page.goto(url, wait_until="networkidle")
|
|
_wait_for_notebook(browser_page, "COURSE_BLUEPRINT.ipynb", "Course Blueprint")
|
|
assert browser_page.locator(".jp-Notebook .jp-RenderedHTMLCommon", has_text="The Four Technical Bands").first.is_visible()
|
|
|
|
|
|
@pytest.mark.browser
|
|
def test_foundations_lab_supports_quiz_answering_and_circuit_editing(
|
|
browser_page,
|
|
jupyter_lab_server,
|
|
):
|
|
_open_notebook_from_directory(
|
|
browser_page,
|
|
jupyter_lab_server,
|
|
"notebooks/foundations/module_01_principles_and_circuit_literacy",
|
|
"lab.ipynb",
|
|
"Principles and Circuit Literacy Lab",
|
|
)
|
|
_run_all_cells(browser_page)
|
|
|
|
browser_page.get_by_label("A. Only one deterministic outcome from the all-zero input").check()
|
|
browser_page.get_by_role("button", name="Check answer").first.click()
|
|
browser_page.locator("text=Correct.").first.wait_for(timeout=120000)
|
|
|
|
editor = _find_code_editor(browser_page)
|
|
editor.fill(
|
|
'circuit = QuantumCircuit(1, 1, name="workflow")\n'
|
|
"circuit.x(0)\n"
|
|
"circuit.measure(0, 0)\n"
|
|
)
|
|
browser_page.get_by_role("button", name="Render circuit").first.click()
|
|
browser_page.locator("pre", has_text="{'1': 256}").first.wait_for(timeout=120000)
|
|
|
|
|
|
@pytest.mark.browser
|
|
def test_capstone_studio_supports_checklist_feedback_and_rubric_workflows(
|
|
browser_page,
|
|
jupyter_lab_server,
|
|
):
|
|
_open_notebook_from_directory(
|
|
browser_page,
|
|
jupyter_lab_server,
|
|
"notebooks/professional/module_04_capstone_design_review",
|
|
"studio.ipynb",
|
|
"Capstone Circuit Design Review Studio",
|
|
)
|
|
_run_all_cells(browser_page)
|
|
|
|
browser_page.get_by_role("button", name="Render circuit").first.wait_for(timeout=120000)
|
|
browser_page.locator("text=Capstone Circuit Design Review Evidence Checklist").first.wait_for(
|
|
timeout=120000
|
|
)
|
|
|
|
browser_page.get_by_placeholder("State the current claim or judgement.").fill(
|
|
"The middle-root GHZ candidate best satisfies the declared local brief."
|
|
)
|
|
browser_page.get_by_placeholder("Name the exact evidence supporting that claim.").fill(
|
|
"It preserves the ideal target, compiles with lower burden on the line, and retains stronger noisy support."
|
|
)
|
|
browser_page.get_by_placeholder("Name the main remaining risk, assumption, or weakness.").fill(
|
|
"The comparison still depends on a synthetic local noise model."
|
|
)
|
|
browser_page.get_by_placeholder("State the next revision, experiment, or rewrite.").fill(
|
|
"Compare the same candidates under a second line-constrained noise profile."
|
|
)
|
|
browser_page.get_by_role("button", name="Review draft").click()
|
|
browser_page.locator("text=Missing sections: None").first.wait_for(timeout=120000)
|
|
|
|
_wait_for_text_in_body(browser_page, "Capstone Circuit Design Review Studio Self-Grading")
|
|
_wait_for_text_in_body(browser_page, "Brief Clarity")
|
|
_wait_for_text_in_body(browser_page, "Total: 0/28")
|
|
_wait_for_text_in_body(browser_page, "Band: revision-needed")
|