All checks were successful
CI / lint (push) Successful in 31s
CI / notebooks-smoke (push) Successful in 1m28s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 13m32s
marimo marks every descendant of a failed or stopped cell with its own
error output ('An ancestor raised an exception', 'ancestor-stopped').
The root cause is always present as a non-ancestor error in the same
snapshot, so cascades only inflate the report — and each one filed its
own deduplicated issue (6 of the 12 nb issues from the first nightly
run were cascade noise). Also makes intentional mo.stop() flow control
count as a pass instead of a failure.
152 lines
4.7 KiB
Python
152 lines
4.7 KiB
Python
"""Tests for dev/scripts/nb_integration.py — session snapshot parsing and
|
|
pass/fail classification.
|
|
|
|
The snapshot fixture mirrors the real shape marimo 0.23.13 writes to
|
|
__marimo__/session/<nb>.py.json (verified empirically): cell errors appear
|
|
as outputs with type "error" plus ename/evalue.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_SCRIPT = Path(__file__).resolve().parents[2] / "dev" / "scripts" / "nb_integration.py"
|
|
_spec = importlib.util.spec_from_file_location("_nb_integration", _SCRIPT)
|
|
assert _spec and _spec.loader
|
|
nbi = importlib.util.module_from_spec(_spec)
|
|
sys.modules["_nb_integration"] = nbi
|
|
_spec.loader.exec_module(nbi)
|
|
|
|
|
|
SNAPSHOT_OK = {
|
|
"version": "1",
|
|
"metadata": {"marimo_version": "0.23.13"},
|
|
"cells": [
|
|
{
|
|
"id": "Hbol",
|
|
"outputs": [{"type": "data", "data": {"text/html": "<pre>2</pre>"}}],
|
|
"console": [],
|
|
}
|
|
],
|
|
}
|
|
|
|
SNAPSHOT_ERR = {
|
|
"version": "1",
|
|
"metadata": {"marimo_version": "0.23.13"},
|
|
"cells": [
|
|
{
|
|
"id": "Hbol",
|
|
"outputs": [{"type": "data", "data": {"text/html": "<pre>2</pre>"}}],
|
|
"console": [],
|
|
},
|
|
{
|
|
"id": "MJUe",
|
|
"outputs": [
|
|
{
|
|
"type": "error",
|
|
"ename": "exception",
|
|
"evalue": "intentional failure 2",
|
|
"traceback": None,
|
|
}
|
|
],
|
|
"console": [
|
|
{
|
|
"type": "stream",
|
|
"name": "stderr",
|
|
"text": "<pre>ValueError: intentional failure 2</pre>",
|
|
"mimetype": "application/vnd.marimo+traceback",
|
|
}
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _write(tmp_path: Path, name: str, snap: dict) -> Path:
|
|
p = tmp_path / name
|
|
p.write_text(json.dumps(snap))
|
|
return p
|
|
|
|
|
|
def test_parse_snapshot_clean(tmp_path):
|
|
errors = nbi.parse_snapshot(_write(tmp_path, "ok.py.json", SNAPSHOT_OK))
|
|
assert errors == []
|
|
|
|
|
|
def test_parse_snapshot_extracts_errors_with_console_detail(tmp_path):
|
|
errors = nbi.parse_snapshot(_write(tmp_path, "err.py.json", SNAPSHOT_ERR))
|
|
assert len(errors) == 1
|
|
e = errors[0]
|
|
assert e["cell"] == "MJUe"
|
|
assert e["ename"] == "exception"
|
|
assert e["evalue"] == "intentional failure 2"
|
|
assert "intentional failure" in e["detail"]
|
|
|
|
|
|
def test_parse_snapshot_drops_cascade_errors(tmp_path):
|
|
"""Descendants of a failed/stopped cell get their own error outputs;
|
|
only the root-cause error should survive parsing."""
|
|
snap = {
|
|
"version": "1",
|
|
"metadata": {"marimo_version": "0.23.13"},
|
|
"cells": [
|
|
SNAPSHOT_ERR["cells"][1],
|
|
{
|
|
"id": "ecfG",
|
|
"outputs": [
|
|
{
|
|
"type": "error",
|
|
"ename": "exception",
|
|
"evalue": "An ancestor raised an exception (ValueError): ",
|
|
"traceback": None,
|
|
}
|
|
],
|
|
"console": [],
|
|
},
|
|
{
|
|
"id": "xXTn",
|
|
"outputs": [
|
|
{
|
|
"type": "error",
|
|
"ename": "ancestor-stopped",
|
|
"evalue": "This cell wasn't run because an ancestor "
|
|
"was stopped with `mo.stop`: ",
|
|
"traceback": None,
|
|
}
|
|
],
|
|
"console": [],
|
|
},
|
|
],
|
|
}
|
|
errors = nbi.parse_snapshot(_write(tmp_path, "cascade.py.json", snap))
|
|
assert [e["cell"] for e in errors] == ["MJUe"]
|
|
|
|
|
|
def test_parse_snapshot_missing_file_reports_export_error(tmp_path):
|
|
errors = nbi.parse_snapshot(tmp_path / "never-written.py.json")
|
|
assert len(errors) == 1
|
|
assert errors[0]["ename"] == "snapshot-missing"
|
|
|
|
|
|
def test_classify_pass_fail_and_expected():
|
|
expected = {"known_bad.py": "duckdb lock (#508)"}
|
|
assert nbi.classify("clean.py", [], expected) == "pass"
|
|
err = [{"cell": "x", "ename": "exception", "evalue": "y", "detail": ""}]
|
|
assert nbi.classify("clean.py", err, expected) == "fail"
|
|
assert nbi.classify("known_bad.py", err, expected) == "expected-fail"
|
|
# an expected-failure notebook that passes should surface as pass
|
|
assert nbi.classify("known_bad.py", [], expected) == "pass"
|
|
|
|
|
|
def test_report_exit_code():
|
|
results = {
|
|
"a.py": {"status": "pass", "errors": []},
|
|
"b.py": {"status": "expected-fail", "errors": [{"ename": "e"}]},
|
|
}
|
|
assert nbi.exit_code(results) == 0
|
|
results["c.py"] = {"status": "fail", "errors": [{"ename": "e"}]}
|
|
assert nbi.exit_code(results) == 1
|