Skip to content

Commit 416cc5b

Browse files
Studio e2e: run Case Study 8 in notebook mode, exercise circuit + Evaluate
1 parent 6de7906 commit 416cc5b

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

studio/tests/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,37 @@ def cs2_dsn() -> str:
182182
admin.execute(f'DROP DATABASE IF EXISTS "{db_name}"')
183183

184184

185+
@pytest.fixture(scope="session")
186+
def cs8_dsn() -> str:
187+
"""A fresh database with only the provsql extension installed, for the
188+
Case Study 8 notebook walkthrough. CS8 is notebook-first and fully
189+
self-contained -- its cells create every table inline -- so the database
190+
needs nothing but the extension. Overridable with
191+
`PROVSQL_STUDIO_CS8_DSN`."""
192+
override = os.environ.get("PROVSQL_STUDIO_CS8_DSN")
193+
if override:
194+
yield override
195+
return
196+
197+
suffix = secrets.token_hex(4)
198+
db_name = f"provsql_studio_cs8_{suffix}"
199+
admin_dsn = "dbname=postgres"
200+
with psycopg.connect(admin_dsn, autocommit=True) as admin:
201+
admin.execute(f'CREATE DATABASE "{db_name}"')
202+
try:
203+
with psycopg.connect(f"dbname={db_name}", autocommit=True) as conn:
204+
conn.execute("CREATE EXTENSION IF NOT EXISTS provsql CASCADE")
205+
yield f"dbname={db_name}"
206+
finally:
207+
with psycopg.connect(admin_dsn, autocommit=True) as admin:
208+
admin.execute(
209+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
210+
"WHERE datname = %s AND pid <> pg_backend_pid()",
211+
(db_name,),
212+
)
213+
admin.execute(f'DROP DATABASE IF EXISTS "{db_name}"')
214+
215+
185216
@pytest.fixture()
186217
def app(test_dsn: str, tmp_path, monkeypatch):
187218
"""Per-test Flask app bound to the test DSN, with the schema search_path

studio/tests/e2e/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def cs2_studio_url(cs2_dsn: str) -> str:
110110
yield from _serve_studio(cs2_dsn, "public")
111111

112112

113+
@pytest.fixture(scope="session")
114+
def cs8_studio_url(cs8_dsn: str) -> str:
115+
"""Studio against the Case Study 8 database (`public` schema; the notebook
116+
creates its own tables)."""
117+
yield from _serve_studio(cs8_dsn, "public")
118+
119+
113120
@pytest.fixture(scope="session")
114121
def browser_context_args(browser_context_args):
115122
# Pin a deterministic viewport so layout-sensitive selectors stay stable
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""End-to-end run of Case Study 8 in **notebook mode**.
2+
3+
CS8 ("ProvSQL as a Probability Calculator") is the notebook-first case study:
4+
a self-contained sequence of one-line probability queries. This test opens the
5+
bundled ``cs8`` example in Studio's notebook mode and runs it end to end
6+
against a live kernel, asserting that every cell executes and none produces an
7+
error banner -- a regression guard that the shipped notebook still runs on the
8+
current ProvSQL.
9+
10+
Backed by the `cs8_studio_url` session fixture (Studio on a fresh provsql
11+
database) and pytest-playwright's `page`."""
12+
from __future__ import annotations
13+
14+
import re
15+
16+
from playwright.sync_api import Page, expect
17+
18+
19+
def test_cs8_notebook_runs_end_to_end(page: Page, cs8_studio_url: str) -> None:
20+
page.goto(cs8_studio_url + "/notebook")
21+
expect(page.locator("body")).to_have_class(
22+
re.compile(r"\bmode-notebook\b"), timeout=8000)
23+
# Start from a pristine notebook, not a previous run's autosaved draft.
24+
page.evaluate("localStorage.removeItem('ps.nb.autosave');"
25+
"localStorage.removeItem('ps.nb.tabs')")
26+
page.reload()
27+
page.wait_for_selector("#notebook-pane", timeout=8000)
28+
29+
# Open the bundled CS8 example; it loads its ~27 code cells.
30+
page.locator("#nb-example").select_option("cs8")
31+
cells = page.locator(".nb-cell--sql")
32+
expect(cells.nth(20)).to_be_attached(timeout=15000)
33+
total = cells.count()
34+
assert total >= 20, total
35+
36+
# Run the whole notebook against a live kernel (binds + creates its own
37+
# tables); wait until the last cell carries an execution count.
38+
page.locator("#nb-run-all").click()
39+
expect(cells.last.locator(".nb-cell__count")).to_have_text(
40+
re.compile(r"\[\d+\]"), timeout=180000)
41+
42+
# A real kernel served the run (not a cached render).
43+
expect(page.locator("#nb-kernel-label")).to_contain_text("pid", timeout=8000)
44+
# Every code cell ran (each shows an execution count) and none errored.
45+
counted = cells.locator(".nb-cell__count").filter(
46+
has_text=re.compile(r"\[\d+\]")).count()
47+
assert counted == total, f"{counted}/{total} cells executed"
48+
assert page.locator(".nb-out .wp-error").count() == 0, "error banner(s)"
49+
# Sanity that real probabilities were computed: the base-rate example's
50+
# unconditional P(positive) = 0.0585 appears in the executed output.
51+
expect(page.locator("#notebook-pane")).to_contain_text("0.0585", timeout=8000)
52+
53+
# --- Exercise the interactive affordances CS8 showcases ---------------
54+
# CS8's headline is the `|` ("given") operator. Click the conditioned
55+
# provenance token it produces (the insulin_resistance-given-obesity row)
56+
# to render its circuit inline, then Evaluate it.
57+
cond_cell = page.locator(
58+
".nb-cell--sql", has_text="factor = 'insulin_resistance'").first
59+
token = cond_cell.locator(".nb-out [data-circuit-uuid]").first
60+
expect(token).to_be_visible(timeout=8000)
61+
token.scroll_into_view_if_needed()
62+
token.click()
63+
circ = page.locator(".nb-cell--circuit").first
64+
expect(circ.locator(".nb-circ__svg .node-group").first).to_be_visible(
65+
timeout=15000)
66+
67+
# The circuit cell's Evaluate button inserts an evaluation cell; run it
68+
# (defaults to marginal probability / exact) and read back a value.
69+
circ.locator(".nb-circ__eval").click()
70+
ev = page.locator(".nb-cell--eval").first
71+
ev.locator(".nb-cell__run").click()
72+
value = ev.locator(".nb-eval__value")
73+
expect(value).not_to_have_text("", timeout=30000)
74+
# P(insulin_resistance | obesity) = 0.5 * 0.6 = 0.3 (casestudy8.rst).
75+
got = float(re.search(r"[0-9]*\.?[0-9]+", value.inner_text()).group())
76+
assert abs(got - 0.3) < 5e-3, value.inner_text()

0 commit comments

Comments
 (0)