Skip to content

Commit 2971668

Browse files
authored
Merge pull request #74 from neo4j/notebook-testing
CI testing for notebooks using Neo4j
2 parents 84c166b + 5fdf630 commit 2971668

File tree

9 files changed

+389
-47
lines changed

9 files changed

+389
-47
lines changed

.github/workflows/gds-integration-tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
tests:
1616
# The type of runner that the job will run on
1717
runs-on: ${{ matrix.os}}
18+
timeout-minutes: 30
1819
strategy:
1920
fail-fast: false
2021
matrix:
@@ -41,4 +42,4 @@ jobs:
4142
AURA_API_CLIENT_ID: 4V1HYCYEeoU4dSxThKnBeLvE2U4hSphx
4243
AURA_API_CLIENT_SECRET: ${{ secrets.AURA_API_CLIENT_SECRET }}
4344
AURA_API_TENANT_ID: eee7ec28-6b1a-5286-8e3a-3362cc1c4c78
44-
run: pytest tests/ --include-neo4j-and-gds -Werror
45+
run: pytest tests/ --include-neo4j-and-gds

.github/workflows/unit-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- run: pip install ".[gds]"
4444

4545
- name: Run tests
46-
run: pytest tests/ -Werror
46+
run: pytest tests/
4747

4848
tests-313:
4949
runs-on: ubuntu-latest
@@ -63,4 +63,4 @@ jobs:
6363
# skip gds for now as it is not available for python 3.13
6464

6565
- name: Run tests
66-
run: pytest tests/ -Werror
66+
run: pytest tests/

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ env.bak/
99
venv.bak/
1010
.conda.sh
1111

12+
out/*
13+
1214
# local env files
1315
.env.local
1416
.env.development.local

examples/gds-nvl-example.ipynb

Lines changed: 71 additions & 34 deletions
Large diffs are not rendered by default.

examples/neo4j-nvl-example.ipynb

Lines changed: 161 additions & 10 deletions
Large diffs are not rendered by default.

python-wrapper/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dev = [
4545
"mypy==1.14.1",
4646
"pytest==8.3.4",
4747
"selenium==4.28.1",
48+
"ipykernel==6.29.5",
4849
"palettable==3.3.3",
4950
"pytest-mock==3.14.0",
5051
"nbconvert==7.16.5",
@@ -64,6 +65,7 @@ notebook = [
6465
"neo4j>=5.26.0",
6566
"ipywidgets>=8.0.0",
6667
"palettable==3.3.3",
68+
"matplotlib==3.10.0",
6769
]
6870

6971
[project.urls]

python-wrapper/tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,20 @@ def gds() -> Generator[Any, None, None]:
3434
else:
3535
api = aura_api()
3636
id, dbms_connection_info = create_aurads_instance(api)
37+
38+
# setting as environment variables to run notebooks with this connection
39+
os.environ["NEO4J_URI"] = dbms_connection_info.uri
40+
os.environ["NEO4J_USER"] = dbms_connection_info.username
41+
os.environ["NEO4J_PASSWORD"] = dbms_connection_info.password
42+
3743
yield GraphDataScience(
3844
endpoint=dbms_connection_info.uri,
3945
auth=(dbms_connection_info.username, dbms_connection_info.password),
4046
aura_ds=True,
4147
database="neo4j",
4248
)
4349

50+
# Clear Neo4j_URI after test (rerun should create a new instance)
51+
os.environ["NEO4J_URI"] = ""
52+
4453
api.delete_instance(id)

python-wrapper/tests/pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[pytest]
22
markers =
33
requires_neo4j_and_gds: mark a test as a requiring a Neo4j instance with GDS running
4+
filterwarnings =
5+
error
6+
ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import pathlib
2+
import signal
3+
import sys
4+
from datetime import datetime
5+
from typing import Any, Callable, NamedTuple
6+
7+
import nbformat
8+
import pytest
9+
from nbclient.exceptions import CellExecutionError
10+
from nbconvert.preprocessors.execute import ExecutePreprocessor
11+
12+
TEARDOWN_CELL_TAG = "teardown"
13+
14+
15+
class IndexedCell(NamedTuple):
16+
cell: Any
17+
index: int # type: ignore
18+
19+
20+
class TeardownExecutePreprocessor(ExecutePreprocessor):
21+
def __init__(self, **kw: Any):
22+
super().__init__(**kw) # type: ignore
23+
24+
def init_notebook(self, tear_down_cells: list[IndexedCell]) -> None:
25+
self.tear_down_cells = tear_down_cells
26+
self._skip_rest = False
27+
28+
# run the cell of a notebook
29+
def preprocess_cell(self, cell: Any, resources: Any, index: int) -> None:
30+
if index == 0:
31+
32+
def handle_signal(sig, frame): # type: ignore
33+
print("Received SIGNAL, running tear down cells")
34+
self.teardown(resources)
35+
sys.exit(1)
36+
37+
signal.signal(signal.SIGINT, handle_signal)
38+
signal.signal(signal.SIGTERM, handle_signal)
39+
40+
try:
41+
if not self._skip_rest:
42+
super().preprocess_cell(cell, resources, index) # type: ignore
43+
except CellExecutionError as e:
44+
if self.tear_down_cells:
45+
print(f"Running tear down cells due to error in notebook execution: {e}")
46+
self.teardown(resources)
47+
raise e
48+
49+
def teardown(self, resources: Any) -> None:
50+
for td_cell, td_idx in self.tear_down_cells:
51+
try:
52+
super().preprocess_cell(td_cell, resources, td_idx) # type: ignore
53+
except CellExecutionError as td_e:
54+
print(f"Error running tear down cell {td_idx}: {td_e}")
55+
56+
57+
class TearDownCollector(ExecutePreprocessor):
58+
def __init__(self, **kw: Any):
59+
super().__init__(**kw) # type: ignore
60+
61+
def init_notebook(self) -> None:
62+
self._tear_down_cells: list[IndexedCell] = []
63+
64+
def preprocess_cell(self, cell: Any, resources: Any, index: int) -> None:
65+
if TEARDOWN_CELL_TAG in cell["metadata"].get("tags", []):
66+
self._tear_down_cells.append(IndexedCell(cell, index))
67+
68+
def tear_down_cells(self) -> list[IndexedCell]:
69+
return self._tear_down_cells
70+
71+
72+
def run_notebooks(filter_func: Callable[[str], bool]) -> None:
73+
current_dir = pathlib.Path(__file__).parent.resolve()
74+
examples_path = current_dir.parent.parent / "examples"
75+
76+
notebook_files = [
77+
f for f in examples_path.iterdir() if f.is_file() and f.suffix == ".ipynb" and filter_func(f.name)
78+
]
79+
80+
ep = TeardownExecutePreprocessor(kernel_name="python3")
81+
td_collector = TearDownCollector(kernel_name="python3")
82+
exceptions: list[RuntimeError] = []
83+
84+
for notebook_filename in notebook_files:
85+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
86+
print(f"{now}: Executing notebook {notebook_filename}", flush=True)
87+
88+
with open(notebook_filename) as f:
89+
nb = nbformat.read(f, as_version=4) # type: ignore
90+
91+
# Collect tear down cells
92+
td_collector.init_notebook()
93+
td_collector.preprocess(nb)
94+
95+
ep.init_notebook(tear_down_cells=td_collector.tear_down_cells())
96+
97+
# run the notebook
98+
try:
99+
ep.preprocess(nb)
100+
print(f"Finished executing notebook {notebook_filename}")
101+
except CellExecutionError as e:
102+
exceptions.append(RuntimeError(f"Error executing notebook {notebook_filename}", e))
103+
continue
104+
105+
if exceptions:
106+
for nb_ex in exceptions:
107+
print(nb_ex)
108+
raise RuntimeError(f"{len(exceptions)} Errors occurred while executing notebooks")
109+
else:
110+
print("Finished executing notebooks")
111+
112+
113+
@pytest.mark.requires_neo4j_and_gds
114+
def test_neo4j(gds: Any) -> None:
115+
neo4j_notebooks = ["neo4j-nvl-example.ipynb", "gds-nvl-example.ipynb"]
116+
117+
def filter_func(notebook: str) -> bool:
118+
return notebook in neo4j_notebooks
119+
120+
run_notebooks(filter_func)
121+
122+
123+
# def test_snowflake() -> None:
124+
# snowflake_notebooks = ["snowflake-nvl-example.ipynb"]
125+
#
126+
# def filter_func(notebook: str) -> bool:
127+
# return notebook in snowflake_notebooks
128+
#
129+
# run_notebooks(filter_func)
130+
#
131+
# def test_simple() -> None:
132+
# simple_notebooks = ["simple-nvl-example.ipynb"]
133+
#
134+
# def filter_func(notebook: str) -> bool:
135+
# return notebook in simple_notebooks
136+
#
137+
# run_notebooks(filter_func)

0 commit comments

Comments
 (0)