From e2ee6f8276127c2ca4a05fcd20a1c650148efeb6 Mon Sep 17 00:00:00 2001 From: xtof Date: Mon, 28 Sep 2026 17:16:10 +0200 Subject: [PATCH 1/3] Add a Colab test notebook for the notebook widget Walks through show(), annotate(), show_instance(), selection, edit-then-show and a larger Liberty design against the released PyPI package; linked from python/README.md with an Open in Colab badge. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 5 +- python/README.md | 3 + python/notebooks/colab_test.ipynb | 153 ++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 python/notebooks/colab_test.ipynb diff --git a/CLAUDE.md b/CLAUDE.md index 780c99f..f4bf980 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,7 +188,10 @@ the wheel and publishes it to PyPI on a `v` tag (version in also reads it into `NAJA_SCHEMATIC_VERSION_STRING` for the C++ app's About dialog, so a release is one edit plus a tag). For local work set `NAJA_SCHEMATIC_BUNDLE` to a locally built bundle (see `_bundle.py`). Tests: -`pytest python/tests`. +`pytest python/tests`. `python/notebooks/colab_test.ipynb` is a manual +end-to-end check of the widget against the released PyPI package (linked +from `python/README.md` as an Open-in-Colab badge); keep it in step with +the `Schematic` API. ## Architecture diff --git a/python/README.md b/python/README.md index ec602ab..d9e037b 100644 --- a/python/README.md +++ b/python/README.md @@ -11,6 +11,9 @@ pip install naja-schematic ## In a notebook (Jupyter, Google Colab, VSCode) +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/najaeda/naja-schematic/blob/main/python/notebooks/colab_test.ipynb) +— a walkthrough of every widget feature below, runnable as is. + ```python from najaeda import netlist import naja_schematic diff --git a/python/notebooks/colab_test.ipynb b/python/notebooks/colab_test.ipynb new file mode 100644 index 0000000..3603faf --- /dev/null +++ b/python/notebooks/colab_test.ipynb @@ -0,0 +1,153 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# naja-schematic in Google Colab \u2014 test notebook\n\nChecks the `naja_schematic` widget end to end in Colab. Run cells top to bottom; each **Check:** line says what to look for." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "!pip install -q naja-schematic\nimport najaeda, naja_schematic, anywidget\nprint(\"naja-schematic\", naja_schematic.__version__, \"| anywidget\", anywidget.__version__)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 1. Small hierarchical design" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "%%writefile fulladder.v\nmodule halfadder(input a, input b, output sum, output carry);\n and carry_and(carry, a, b);\n xor sum_xor(sum, a, b);\nendmodule\n\nmodule fulladder(input a, input b, input cin, output sum, output cout);\n wire sum1, carry1, carry2;\n halfadder ha1(.a(a), .b(b), .sum(sum1), .carry(carry1));\n halfadder ha2(.a(sum1), .b(cin), .sum(sum), .carry(carry2));\n or cout_or(cout, carry1, carry2);\nendmodule" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "from najaeda import netlist\nnetlist.reset()\ntop = netlist.load_verilog(\"fulladder.v\")\ntop.get_name()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 2. Basic view\n**Check:** viewer renders; tree shows `fulladder`; expanding `ha1`/`ha2` lazily loads instances, terms and nets; right-click a term \u2192 *Show Equipotential* / *Trace to Driver* draws a schematic; clicking an open pin grows it; *Show Properties* fills the Properties tab." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "view = naja_schematic.show(height=600)\nview" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 3. Diagnosis overlay (push into an existing view)\n**Check:** in the view above, `ha1` turns red in the tree and schematic, the `sum1` wire/pin is tinted, and the Diagnosis panel lists both items." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "view.annotate([\n {\"kind\": \"instance\", \"path\": [\"ha1\"], \"severity\": \"error\",\n \"message\": \"not equivalent\", \"source\": \"kepler-formal\"},\n {\"kind\": \"net\", \"path\": [\"ha1\"], \"terminal\": \"sum\", \"severity\": \"warning\",\n \"message\": \"suspicious output\", \"source\": \"colab-test\"},\n])" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 4. Host \u2192 viewer: focus an instance\n**Check:** tree opens down to `ha2/sum_xor`, it's selected, Properties shows it, schematic draws it alone with open pins." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "view.show_instance(\"ha2/sum_xor\")" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "# Same thing with a najaeda Instance\nview.show_instance(top.get_child_instance(\"ha1\").get_child_instance(\"carry_and\"))" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 5. Viewer \u2192 host: selection\nClick an instance in the tree or a box in the schematic, then run the next cell. The callback prints on each click (output appears under this cell)." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "handler = view.on_select(lambda inst: print(\"selected:\", inst))" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "print(\"selected_path:\", view.selected_path)\ninst = view.selected\nprint(inst, \"->\", inst.get_model_name() if inst else None)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 6. One-shot view with diagnosis + instance" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "naja_schematic.show(\n height=500,\n instance=\"ha1\",\n diagnosis=[{\"kind\": \"instance\", \"path\": [\"ha2\"], \"severity\": \"info\",\n \"message\": \"hello from show()\", \"source\": \"colab-test\"}],\n)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 7. Edit, then a fresh view\n**Check:** the new view shows `halfadder1` instead of `ha1` (the older view above does not refresh \u2014 known gap)." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "top.get_child_instance(\"ha1\").set_name(\"halfadder1\")\nnaja_schematic.show(height=500)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 8. Larger gate-level design with Liberty (tinyrocket)\n**Check:** loads in reasonable time; tree scrolls; tracing a flop input to driver works and stays responsive." + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": null, + "outputs": [], + "source": "!wget -q https://github.com/najaeda/naja/archive/refs/heads/main.zip -O naja-main.zip && unzip -q -o naja-main.zip \"naja-main/tutorials/benchmarks/*\"\nbench = \"naja-main/tutorials/benchmarks\"\nnetlist.reset()\nnetlist.load_liberty([f\"{bench}/liberty/tutorial_cells.lib\", f\"{bench}/liberty/fakeram45_64x32.lib\"])\ntop = netlist.load_verilog(f\"{bench}/verilog/tinyrocket/tinyrocket.v\")\nnaja_schematic.show(height=700)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## Things to note while testing\n- Mouse wheel over the viewer doesn't scroll the page (known).\n- Two/three views on the page at once: do they all stay interactive?\n- Keyboard input only goes to the focused viewer?\n- Any errors in the browser console (F12) or the Colab cell output?" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "colab": { + "provenance": [] + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 5d06d86a4c20dde56f5eea4459c61263929b5bab Mon Sep 17 00:00:00 2001 From: xtof Date: Mon, 28 Sep 2026 17:19:15 +0200 Subject: [PATCH 2/3] Run the Colab notebook in CI; move its badge to the top README tests/test_notebook.py executes python/notebooks/colab_test.ipynb in a real IPython kernel via nbclient, with a spliced-in cell that plays the viewer against the notebook's view (load_root, resolve_instance, get_properties, instance_selected). The notebook's install cell now skips pip when naja_schematic is importable, so CI exercises the built wheel. nbformat, nbclient and ipykernel join the test extra. The download uses curl, which every CI runner and Colab have. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 11 +- README.md | 1 + python/README.md | 4 +- python/notebooks/colab_test.ipynb | 195 ++++++++++++++++++++++++------ python/pyproject.toml | 3 +- python/tests/test_notebook.py | 88 ++++++++++++++ 6 files changed, 257 insertions(+), 45 deletions(-) create mode 100644 python/tests/test_notebook.py diff --git a/CLAUDE.md b/CLAUDE.md index f4bf980..b142be2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,10 +188,13 @@ the wheel and publishes it to PyPI on a `v` tag (version in also reads it into `NAJA_SCHEMATIC_VERSION_STRING` for the C++ app's About dialog, so a release is one edit plus a tag). For local work set `NAJA_SCHEMATIC_BUNDLE` to a locally built bundle (see `_bundle.py`). Tests: -`pytest python/tests`. `python/notebooks/colab_test.ipynb` is a manual -end-to-end check of the widget against the released PyPI package (linked -from `python/README.md` as an Open-in-Colab badge); keep it in step with -the `Schematic` API. +`pytest python/tests`. `python/notebooks/colab_test.ipynb` is the +Open-in-Colab notebook (badge in the top `README.md`): a walkthrough of the +widget against the released PyPI package. `tests/test_notebook.py` runs it +in a real kernel (nbclient) and splices in a cell that plays the viewer +against its `view`, so a `Schematic`/protocol change that breaks it fails +CI; its install cell skips `pip install` when `naja_schematic` is already +importable, so CI tests the fresh wheel, not PyPI's. ## Architecture diff --git a/README.md b/README.md index 76ad8de..d6ea4f8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Native macOS Build](https://github.com/najaeda/naja-schematic/actions/workflows/native-macos.yml/badge.svg?branch=main)](https://github.com/najaeda/naja-schematic/actions/workflows/native-macos.yml) [![Native Linux Build](https://github.com/najaeda/naja-schematic/actions/workflows/native-linux.yml/badge.svg?branch=main)](https://github.com/najaeda/naja-schematic/actions/workflows/native-linux.yml) [![Emscripten Build](https://github.com/najaeda/naja-schematic/actions/workflows/emscripten.yml/badge.svg?branch=main)](https://github.com/najaeda/naja-schematic/actions/workflows/emscripten.yml) +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/najaeda/naja-schematic/blob/main/python/notebooks/colab_test.ipynb) > **Note:** This is a proof of concept in beta. Expect rough edges, breaking diff --git a/python/README.md b/python/README.md index d9e037b..4b9b3d9 100644 --- a/python/README.md +++ b/python/README.md @@ -11,8 +11,8 @@ pip install naja-schematic ## In a notebook (Jupyter, Google Colab, VSCode) -[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/najaeda/naja-schematic/blob/main/python/notebooks/colab_test.ipynb) -— a walkthrough of every widget feature below, runnable as is. +[Try it in Colab](https://colab.research.google.com/github/najaeda/naja-schematic/blob/main/python/notebooks/colab_test.ipynb): +a walkthrough of every widget feature below, runnable as is. ```python from najaeda import netlist diff --git a/python/notebooks/colab_test.ipynb b/python/notebooks/colab_test.ipynb index 3603faf..b8127e9 100644 --- a/python/notebooks/colab_test.ipynb +++ b/python/notebooks/colab_test.ipynb @@ -2,140 +2,262 @@ "cells": [ { "cell_type": "markdown", + "id": "9a83c378", "metadata": {}, - "source": "# naja-schematic in Google Colab \u2014 test notebook\n\nChecks the `naja_schematic` widget end to end in Colab. Run cells top to bottom; each **Check:** line says what to look for." + "source": [ + "# naja-schematic in Google Colab — test notebook\n", + "\n", + "Checks the `naja_schematic` widget end to end in Colab. Run cells top to bottom; each **Check:** line says what to look for." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "d2afb533", + "metadata": {}, "outputs": [], - "source": "!pip install -q naja-schematic\nimport najaeda, naja_schematic, anywidget\nprint(\"naja-schematic\", naja_schematic.__version__, \"| anywidget\", anywidget.__version__)" + "source": [ + "# Installs the released package (Colab); skipped where naja_schematic is\n", + "# already installed, e.g. when CI runs this notebook against a fresh wheel.\n", + "import importlib.util\n", + "if importlib.util.find_spec(\"naja_schematic\") is None:\n", + " %pip install -q naja-schematic\n", + "\n", + "import najaeda, naja_schematic, anywidget\n", + "print(\"naja-schematic\", naja_schematic.__version__, \"| anywidget\", anywidget.__version__)" + ] }, { "cell_type": "markdown", + "id": "3fd7c1a6", "metadata": {}, - "source": "## 1. Small hierarchical design" + "source": [ + "## 1. Small hierarchical design" + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "36ae7db0", + "metadata": {}, "outputs": [], - "source": "%%writefile fulladder.v\nmodule halfadder(input a, input b, output sum, output carry);\n and carry_and(carry, a, b);\n xor sum_xor(sum, a, b);\nendmodule\n\nmodule fulladder(input a, input b, input cin, output sum, output cout);\n wire sum1, carry1, carry2;\n halfadder ha1(.a(a), .b(b), .sum(sum1), .carry(carry1));\n halfadder ha2(.a(sum1), .b(cin), .sum(sum), .carry(carry2));\n or cout_or(cout, carry1, carry2);\nendmodule" + "source": [ + "%%writefile fulladder.v\n", + "module halfadder(input a, input b, output sum, output carry);\n", + " and carry_and(carry, a, b);\n", + " xor sum_xor(sum, a, b);\n", + "endmodule\n", + "\n", + "module fulladder(input a, input b, input cin, output sum, output cout);\n", + " wire sum1, carry1, carry2;\n", + " halfadder ha1(.a(a), .b(b), .sum(sum1), .carry(carry1));\n", + " halfadder ha2(.a(sum1), .b(cin), .sum(sum), .carry(carry2));\n", + " or cout_or(cout, carry1, carry2);\n", + "endmodule" + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "9bfd1fb5", + "metadata": {}, "outputs": [], - "source": "from najaeda import netlist\nnetlist.reset()\ntop = netlist.load_verilog(\"fulladder.v\")\ntop.get_name()" + "source": [ + "from najaeda import netlist\n", + "netlist.reset()\n", + "top = netlist.load_verilog(\"fulladder.v\")\n", + "top.get_name()" + ] }, { "cell_type": "markdown", + "id": "45082e18", "metadata": {}, - "source": "## 2. Basic view\n**Check:** viewer renders; tree shows `fulladder`; expanding `ha1`/`ha2` lazily loads instances, terms and nets; right-click a term \u2192 *Show Equipotential* / *Trace to Driver* draws a schematic; clicking an open pin grows it; *Show Properties* fills the Properties tab." + "source": [ + "## 2. Basic view\n", + "**Check:** viewer renders; tree shows `fulladder`; expanding `ha1`/`ha2` lazily loads instances, terms and nets; right-click a term → *Show Equipotential* / *Trace to Driver* draws a schematic; clicking an open pin grows it; *Show Properties* fills the Properties tab." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "9cac3fc0", + "metadata": {}, "outputs": [], - "source": "view = naja_schematic.show(height=600)\nview" + "source": [ + "view = naja_schematic.show(height=600)\n", + "view" + ] }, { "cell_type": "markdown", + "id": "f9cbf15b", "metadata": {}, - "source": "## 3. Diagnosis overlay (push into an existing view)\n**Check:** in the view above, `ha1` turns red in the tree and schematic, the `sum1` wire/pin is tinted, and the Diagnosis panel lists both items." + "source": [ + "## 3. Diagnosis overlay (push into an existing view)\n", + "**Check:** in the view above, `ha1` turns red in the tree and schematic, the `sum1` wire/pin is tinted, and the Diagnosis panel lists both items." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "e81283ce", + "metadata": {}, "outputs": [], - "source": "view.annotate([\n {\"kind\": \"instance\", \"path\": [\"ha1\"], \"severity\": \"error\",\n \"message\": \"not equivalent\", \"source\": \"kepler-formal\"},\n {\"kind\": \"net\", \"path\": [\"ha1\"], \"terminal\": \"sum\", \"severity\": \"warning\",\n \"message\": \"suspicious output\", \"source\": \"colab-test\"},\n])" + "source": [ + "view.annotate([\n", + " {\"kind\": \"instance\", \"path\": [\"ha1\"], \"severity\": \"error\",\n", + " \"message\": \"not equivalent\", \"source\": \"kepler-formal\"},\n", + " {\"kind\": \"net\", \"path\": [\"ha1\"], \"terminal\": \"sum\", \"severity\": \"warning\",\n", + " \"message\": \"suspicious output\", \"source\": \"colab-test\"},\n", + "])" + ] }, { "cell_type": "markdown", + "id": "fa05dd8c", "metadata": {}, - "source": "## 4. Host \u2192 viewer: focus an instance\n**Check:** tree opens down to `ha2/sum_xor`, it's selected, Properties shows it, schematic draws it alone with open pins." + "source": [ + "## 4. Host → viewer: focus an instance\n", + "**Check:** tree opens down to `ha2/sum_xor`, it's selected, Properties shows it, schematic draws it alone with open pins." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "e2f5219d", + "metadata": {}, "outputs": [], - "source": "view.show_instance(\"ha2/sum_xor\")" + "source": [ + "view.show_instance(\"ha2/sum_xor\")" + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "b40b4288", + "metadata": {}, "outputs": [], - "source": "# Same thing with a najaeda Instance\nview.show_instance(top.get_child_instance(\"ha1\").get_child_instance(\"carry_and\"))" + "source": [ + "# Same thing with a najaeda Instance\n", + "view.show_instance(top.get_child_instance(\"ha1\").get_child_instance(\"carry_and\"))" + ] }, { "cell_type": "markdown", + "id": "2966c079", "metadata": {}, - "source": "## 5. Viewer \u2192 host: selection\nClick an instance in the tree or a box in the schematic, then run the next cell. The callback prints on each click (output appears under this cell)." + "source": [ + "## 5. Viewer → host: selection\n", + "Click an instance in the tree or a box in the schematic, then run the next cell. The callback prints on each click (output appears under this cell)." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "e2125d57", + "metadata": {}, "outputs": [], - "source": "handler = view.on_select(lambda inst: print(\"selected:\", inst))" + "source": [ + "handler = view.on_select(lambda inst: print(\"selected:\", inst))" + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "4ab2dcc9", + "metadata": {}, "outputs": [], - "source": "print(\"selected_path:\", view.selected_path)\ninst = view.selected\nprint(inst, \"->\", inst.get_model_name() if inst else None)" + "source": [ + "print(\"selected_path:\", view.selected_path)\n", + "inst = view.selected\n", + "print(inst, \"->\", inst.get_model_name() if inst else None)" + ] }, { "cell_type": "markdown", + "id": "854f5c0f", "metadata": {}, - "source": "## 6. One-shot view with diagnosis + instance" + "source": [ + "## 6. One-shot view with diagnosis + instance" + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "7b22e0f3", + "metadata": {}, "outputs": [], - "source": "naja_schematic.show(\n height=500,\n instance=\"ha1\",\n diagnosis=[{\"kind\": \"instance\", \"path\": [\"ha2\"], \"severity\": \"info\",\n \"message\": \"hello from show()\", \"source\": \"colab-test\"}],\n)" + "source": [ + "naja_schematic.show(\n", + " height=500,\n", + " instance=\"ha1\",\n", + " diagnosis=[{\"kind\": \"instance\", \"path\": [\"ha2\"], \"severity\": \"info\",\n", + " \"message\": \"hello from show()\", \"source\": \"colab-test\"}],\n", + ")" + ] }, { "cell_type": "markdown", + "id": "5d91666e", "metadata": {}, - "source": "## 7. Edit, then a fresh view\n**Check:** the new view shows `halfadder1` instead of `ha1` (the older view above does not refresh \u2014 known gap)." + "source": [ + "## 7. Edit, then a fresh view\n", + "**Check:** the new view shows `halfadder1` instead of `ha1` (the older view above does not refresh — known gap)." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "b86790fd", + "metadata": {}, "outputs": [], - "source": "top.get_child_instance(\"ha1\").set_name(\"halfadder1\")\nnaja_schematic.show(height=500)" + "source": [ + "top.get_child_instance(\"ha1\").set_name(\"halfadder1\")\n", + "naja_schematic.show(height=500)" + ] }, { "cell_type": "markdown", + "id": "8a71293c", "metadata": {}, - "source": "## 8. Larger gate-level design with Liberty (tinyrocket)\n**Check:** loads in reasonable time; tree scrolls; tracing a flop input to driver works and stays responsive." + "source": [ + "## 8. Larger gate-level design with Liberty (tinyrocket)\n", + "**Check:** loads in reasonable time; tree scrolls; tracing a flop input to driver works and stays responsive." + ] }, { "cell_type": "code", - "metadata": {}, "execution_count": null, + "id": "ac9e690a", + "metadata": {}, "outputs": [], - "source": "!wget -q https://github.com/najaeda/naja/archive/refs/heads/main.zip -O naja-main.zip && unzip -q -o naja-main.zip \"naja-main/tutorials/benchmarks/*\"\nbench = \"naja-main/tutorials/benchmarks\"\nnetlist.reset()\nnetlist.load_liberty([f\"{bench}/liberty/tutorial_cells.lib\", f\"{bench}/liberty/fakeram45_64x32.lib\"])\ntop = netlist.load_verilog(f\"{bench}/verilog/tinyrocket/tinyrocket.v\")\nnaja_schematic.show(height=700)" + "source": [ + "!curl -sSfL https://github.com/najaeda/naja/archive/refs/heads/main.zip -o naja-main.zip && unzip -q -o naja-main.zip \"naja-main/tutorials/benchmarks/*\"\n", + "bench = \"naja-main/tutorials/benchmarks\"\n", + "netlist.reset()\n", + "netlist.load_liberty([f\"{bench}/liberty/tutorial_cells.lib\", f\"{bench}/liberty/fakeram45_64x32.lib\"])\n", + "top = netlist.load_verilog(f\"{bench}/verilog/tinyrocket/tinyrocket.v\")\n", + "naja_schematic.show(height=700)" + ] }, { "cell_type": "markdown", + "id": "cbbaad03", "metadata": {}, - "source": "## Things to note while testing\n- Mouse wheel over the viewer doesn't scroll the page (known).\n- Two/three views on the page at once: do they all stay interactive?\n- Keyboard input only goes to the focused viewer?\n- Any errors in the browser console (F12) or the Colab cell output?" + "source": [ + "## Things to note while testing\n", + "- Mouse wheel over the viewer doesn't scroll the page (known).\n", + "- Two/three views on the page at once: do they all stay interactive?\n", + "- Keyboard input only goes to the focused viewer?\n", + "- Any errors in the browser console (F12) or the Colab cell output?" + ] } ], "metadata": { + "colab": { + "provenance": [] + }, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -143,11 +265,8 @@ }, "language_info": { "name": "python" - }, - "colab": { - "provenance": [] } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/python/pyproject.toml b/python/pyproject.toml index f90dd15..012bb31 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -22,7 +22,8 @@ classifiers = [ ] [project.optional-dependencies] -test = ["pytest"] +# nbformat/nbclient/ipykernel: tests/test_notebook.py runs the Colab notebook. +test = ["pytest", "nbformat", "nbclient", "ipykernel"] [project.scripts] naja-schematic = "naja_schematic.server:main" diff --git a/python/tests/test_notebook.py b/python/tests/test_notebook.py new file mode 100644 index 0000000..0c5f0b7 --- /dev/null +++ b/python/tests/test_notebook.py @@ -0,0 +1,88 @@ +"""Runs python/notebooks/colab_test.ipynb (the Open-in-Colab notebook) in a +real IPython kernel, so an API change that breaks it fails CI. + +The kernel side is all that can run headless: there's no browser to render +the widget, so a cell spliced in mid-notebook plays the viewer against the +notebook's own `view` (requests in over the comm, replies captured). +""" +import json +from pathlib import Path + +import pytest + +nbformat = pytest.importorskip("nbformat") +nbclient = pytest.importorskip("nbclient") +pytest.importorskip("ipykernel") +pytest.importorskip("anywidget") + +from naja_schematic._bundle import bundle_path # noqa: E402 + +NOTEBOOK = Path(__file__).parents[1] / "notebooks" / "colab_test.ipynb" +WIDGET_MIME = "application/vnd.jupyter.widget-view+json" + +# Stands in for the viewer while the full adder is loaded: every request +# must be answered, and the viewer -> host selection must reach najaeda. +VIEWER_CHECK = r''' +import json +_sent = [] +view.send = lambda content, buffers=None: _sent.append(json.loads(content["json"])) + +def _ask(request): + del _sent[:] + view._on_viewer_message(view, {"json": json.dumps(request)}, []) + return list(_sent) + +replies = _ask({"request": "load_root"}) +assert [r["response"] for r in replies] == \ + ["root_response", "diagnosis_response", "focus_instance"], replies +assert replies[0]["root"]["name"] == "fulladder", replies[0] +assert replies[2]["path"] == ["ha1", "carry_and"], replies[2] + +(resolved,) = _ask({"request": "resolve_instance", "path": ["ha2", "sum_xor"]}) +assert resolved["response"] == "instance_resolved" and resolved["found"], resolved + +(props,) = _ask({"request": "get_properties", "kind": "instance", "path": ["ha1"]}) +assert {"name": "Model", "value": "halfadder"} in props["properties"], props + +assert _ask({"request": "instance_selected", "path": ["ha2"]}) == [] +assert view.selected_path == ["ha2"] +assert view.selected.get_name() == "ha2" +''' + + +@pytest.fixture +def notebook_env(tmp_path, monkeypatch): + # Without a built bundle (a source checkout), give the widget a stub: + # nothing renders it here anyway. The kernel inherits the environment. + if bundle_path() is None: + stub = tmp_path / "naja-schematic.js" + stub.write_text("var createNajaSchematic;") + monkeypatch.setenv("NAJA_SCHEMATIC_BUNDLE", str(stub)) + return tmp_path + + +def splice_viewer_check(nb): + # Right after the selection cells, before the design is edited. + at = next(i for i, cell in enumerate(nb.cells) + if cell.cell_type == "code" and "view.selected_path" in cell.source) + nb.cells.insert(at + 1, nbformat.v4.new_code_cell(VIEWER_CHECK)) + + +def test_colab_notebook_runs(notebook_env): + nb = nbformat.read(NOTEBOOK, as_version=4) + splice_viewer_check(nb) + client = nbclient.NotebookClient( + nb, kernel_name="python3", timeout=600, + resources={"metadata": {"path": str(notebook_env)}}) + # Raises CellExecutionError, with the failing cell, on any exception. + client.execute() + + code_cells = [c for c in nb.cells if c.cell_type == "code"] + # The install cell must not have reinstalled over the package under test. + first = "".join(o.get("text", "") for o in code_cells[0].outputs) + assert "naja-schematic" in first and "Successfully installed" not in first + + views = [c for c in code_cells + if any(WIDGET_MIME in o.get("data", {}) for o in c.outputs)] + shows = [c for c in code_cells if "show(" in c.source] + assert len(views) == len(shows) >= 4 From b2df7077fc65eae3bc026eb3f23df244ae2b6f9c Mon Sep 17 00:00:00 2001 From: xtof Date: Mon, 28 Sep 2026 17:24:36 +0200 Subject: [PATCH 3/3] Add REUSE annotations for the Colab notebook and its test Co-Authored-By: Claude Opus 5.5 --- REUSE.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index 6f7d54e..40b0eda 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -29,9 +29,11 @@ path = [ "python/naja_schematic/static/index.html", "python/naja_schematic/static/widget.js", "python/naja_schematic/widget.py", + "python/notebooks/colab_test.ipynb", "python/pyproject.toml", "python/tests/conftest.py", "python/tests/data/design.v", + "python/tests/test_notebook.py", "python/tests/test_protocol.py", "python/tests/test_transports.py", "python/tests/test_widget.py",