Skip to content

Commit 8186d35

Browse files
committed
Capture stdout/stderr in server test and align completion timing
Update the language server test cell code to use stdout/stderr. Inserts a short 20ms delay before setting the completion event to ensure we capture the stdout/stderr, which are buffered and flushed every 10ms. Ideally, we could flush prior to the "idle" cell-op for each cell run, but that requires more complicated upstream change so this works for now. We will do the same thing in the extension front end.
1 parent 4ea1020 commit 8186d35

File tree

2 files changed

+84
-12
lines changed

2 files changed

+84
-12
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ ignore = [
5454
"SLF001",
5555
"PLC",
5656
"PLR",
57+
"FIX",
5758
]

tests/test_server.py

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
from pytest_lsp import ClientServerConfig, LanguageClient
1010

1111

12-
def to_dict(obj: Any) -> dict | list: # noqa: ANN401
12+
def asdict(obj: Any) -> dict[str, Any]: # noqa: ANN401
1313
"""Recursively convert namedtuple Objects to dicts."""
1414
if hasattr(obj, "_asdict"):
15-
return {k: to_dict(v) for k, v in obj._asdict().items()}
15+
return {k: asdict(v) for k, v in obj._asdict().items()}
1616
if isinstance(obj, list):
17-
return [to_dict(item) for item in obj]
17+
# Just used recursively
18+
return [asdict(item) for item in obj] # pyright: ignore[reportReturnType]
1819
if isinstance(obj, dict):
19-
return {k: to_dict(v) for k, v in obj.items()}
20+
return {k: asdict(v) for k, v in obj.items()}
2021
return obj
2122

2223

@@ -298,6 +299,13 @@ def __():
298299
@pytest.mark.asyncio
299300
async def test_simple_marimo_run(client: LanguageClient) -> None:
300301
"""Test that we can collect marimo operations until cell reaches idle state."""
302+
code = """\
303+
import sys
304+
305+
print("hello, world")
306+
print("error message", file=sys.stderr)
307+
x = 42\
308+
"""
301309

302310
client.notebook_document_did_open(
303311
lsp.DidOpenNotebookDocumentParams(
@@ -317,7 +325,7 @@ async def test_simple_marimo_run(client: LanguageClient) -> None:
317325
uri="file:///exec_test.py#cell1",
318326
language_id="python",
319327
version=1,
320-
text="x = 42",
328+
text=code,
321329
)
322330
],
323331
),
@@ -327,11 +335,20 @@ async def test_simple_marimo_run(client: LanguageClient) -> None:
327335
completion_event = asyncio.Event()
328336

329337
@client.feature("marimo/operation")
330-
def on_marimo_operation(params: Any) -> None: # noqa: ANN401
338+
async def on_marimo_operation(params: Any) -> None: # noqa: ANN401
331339
# pygls dynamically makes an `Object` named tuple which makes snapshotting hard
332340
# we just convert to a regular dict here for snapshotting
333-
messages.append(to_dict(params))
341+
msg = asdict(params)
342+
# Sort variables lists for consistent ordering in tests
343+
if msg.get("op") in ("variables", "variable-values"):
344+
msg["data"]["variables"] = sorted(
345+
msg["data"]["variables"], key=lambda x: x["name"]
346+
)
347+
messages.append(msg)
334348
if params.op == "completed-run":
349+
# FIXME: stdin/stdout are flushed every 10ms, so wait 100ms to ensure
350+
# all related events. The frontend uses the same workaround.
351+
await asyncio.sleep(0.1)
335352
completion_event.set()
336353

337354
await client.workspace_execute_command_async(
@@ -341,7 +358,7 @@ def on_marimo_operation(params: Any) -> None: # noqa: ANN401
341358
{
342359
"notebook_uri": "file:///exec_test.py",
343360
"cell_ids": ["cell1"],
344-
"codes": ["x = 42"],
361+
"codes": [code],
345362
}
346363
],
347364
)
@@ -355,7 +372,15 @@ def on_marimo_operation(params: Any) -> None: # noqa: ANN401
355372
"op": "update-cell-codes",
356373
"data": {
357374
"cell_ids": ["cell1"],
358-
"codes": ["x = 42"],
375+
"codes": [
376+
"""\
377+
import sys
378+
379+
print("hello, world")
380+
print("error message", file=sys.stderr)
381+
x = 42\
382+
"""
383+
],
359384
"code_is_stale": False,
360385
},
361386
},
@@ -369,7 +394,8 @@ def on_marimo_operation(params: Any) -> None: # noqa: ANN401
369394
"op": "variables",
370395
"data": {
371396
"variables": [
372-
{"name": "x", "declared_by": ["cell1"], "used_by": []}
397+
{"name": "sys", "declared_by": ["cell1"], "used_by": []},
398+
{"name": "x", "declared_by": ["cell1"], "used_by": []},
373399
]
374400
},
375401
},
@@ -410,7 +436,10 @@ def on_marimo_operation(params: Any) -> None: # noqa: ANN401
410436
"notebookUri": "file:///exec_test.py",
411437
"op": "variable-values",
412438
"data": {
413-
"variables": [{"name": "x", "value": "42", "datatype": "int"}]
439+
"variables": [
440+
{"name": "sys", "value": "sys", "datatype": "module"},
441+
{"name": "x", "value": "42", "datatype": "int"},
442+
]
414443
},
415444
},
416445
{
@@ -446,6 +475,48 @@ def on_marimo_operation(params: Any) -> None: # noqa: ANN401
446475
"timestamp": IsFloat(),
447476
},
448477
},
449-
{"notebookUri": "file:///exec_test.py", "op": "completed-run", "data": {}},
478+
{
479+
"notebookUri": "file:///exec_test.py",
480+
"op": "completed-run",
481+
"data": {},
482+
},
483+
{
484+
"notebookUri": "file:///exec_test.py",
485+
"op": "cell-op",
486+
"data": {
487+
"cell_id": "cell1",
488+
"output": None,
489+
"console": {
490+
"channel": "stdout",
491+
"mimetype": "text/plain",
492+
"data": "hello, world\n",
493+
"timestamp": IsFloat(),
494+
},
495+
"status": None,
496+
"stale_inputs": None,
497+
"run_id": None,
498+
"serialization": None,
499+
"timestamp": IsFloat(),
500+
},
501+
},
502+
{
503+
"notebookUri": "file:///exec_test.py",
504+
"op": "cell-op",
505+
"data": {
506+
"cell_id": "cell1",
507+
"output": None,
508+
"console": {
509+
"channel": "stderr",
510+
"mimetype": "text/plain",
511+
"data": "error message\n",
512+
"timestamp": IsFloat(),
513+
},
514+
"status": None,
515+
"stale_inputs": None,
516+
"run_id": None,
517+
"serialization": None,
518+
"timestamp": IsFloat(),
519+
},
520+
},
450521
]
451522
)

0 commit comments

Comments
 (0)