Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ markers = [
"unit: unit tests",
"json_files: Export JSON files with --update-json-files",
"out_files: Export .out fixture files with --update-out-files",
"notebooks: tests that run Jupyter notebooks",
]


Expand Down
81 changes: 81 additions & 0 deletions tests/test_notebook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python
"""Run all notebooks in tests/notebooks/ and report pass/fail."""

import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path

import pytest

NOTEBOOKS_DIR = Path(__file__).parent.parent / "docs/contents/notebooks"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding these complex paths, I think we should start adding symlinks to the repo. If we refactor the layout at some point, finding broken symlinks, is a peace of cake, but correcting the hardcoded paths is painful.

@haneug
Objections?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, sounds good!

BLACKLIST = ["extopt", "ir_spectrum", "ml_properties"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are they blacklisted?
Please add a comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they require additional external dependencies that cannot easily be set up by hand (extopt and ml_properties at least). This should be documented here. About ir_spectrum I am not sure?



def run_notebook(nb: Path) -> tuple[bool, float, str]:
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please complete the docstring.

Helper function that runs a specific notebook and reports pass/fail.
Parameters
----------
nb

Returns
-------

"""
start = time.monotonic()
with tempfile.TemporaryDirectory() as tmp:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Describe in detail what you are doing here:

  • Where are notebooks executed?
  • How are they executed?
  • Why is there a timeout?
  • Why does it need to copied over?

Where do make sure jupyter is installed?

nb_copy = Path(tmp) / nb.name
shutil.copy2(nb, nb_copy)
result = subprocess.run(
[
"jupyter",
"nbconvert",
"--execute",
"--to",
"notebook",
"--ExecutePreprocessor.timeout=1800",
"--output",
nb.name,
"--output-dir",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess --output-dir is redundant, as cwd=tmp.

tmp,
str(nb_copy),
],
capture_output=True,
text=True,
cwd=tmp,
)
elapsed = time.monotonic() - start
return result.returncode == 0, elapsed, result.stdout + result.stderr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea with the boolean!



@pytest.mark.notebooks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notebooks also always run orca so I guess @pytest.mark.orca is also appropriate here

def test_notebooks() -> None:
"""
Function to iteratively test whether all notebooks (with exception to the ones to be excluded, which are defined in `BlACKLIST`) execute

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in BlACKLIST

without issues. In the case that a notebook execution fails,the function will print out which notebooks fail and the error message.
"""
notebooks = [nb for nb in NOTEBOOKS_DIR.glob("*.ipynb") if nb.stem not in BLACKLIST]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should actually be a fixture.

Instead of one master function like here that acts as whole notebooks testsuite, I would boil this down to a single function, that takes a notebook, executes it and returns a flag if the notebook failed or not.

I would also keep the reporting to a minimum. Pytests captures the output anyway.
Though we should keep the printing of the timings. This is good for debugging and make sure out testsuite does not take too long ;)

I think @haneug already wrote a custom summary report for one type of tests that we have. Maybe he can help.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the fixture / parametrize. I did add a custom error message when example tests fail although I do not remember much of this I am happy to discuss this.

if not notebooks:
print(f"No valid notebooks found in {NOTEBOOKS_DIR}")
sys.exit(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use pytest.fail() instead


results: list[tuple[str, bool, float]] = []
for nb in notebooks:
print(f" running {nb.stem} ...", end="", flush=True)
ok, elapsed, output = run_notebook(nb)
status = "PASS" if ok else "FAIL"
print(f" {status} ({elapsed:.1f}s)")
if not ok:
print(output)
results.append((nb.name, ok, elapsed))

failures = [name for name, ok, _ in results if not ok]
print(f"{len(notebooks) - len(failures)}/{len(notebooks)} notebooks passed")
if failures:
print("Failed:")
for name in failures:
print(f" - {name}")
sys.exit(1)
Loading