Skip to content

Commit e4b29e2

Browse files
committed
Allow replacing notebooks content pre-test
1 parent 05da472 commit e4b29e2

File tree

1 file changed

+108
-19
lines changed

1 file changed

+108
-19
lines changed

tests/utils_for_testbook.py

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import json
2-
import warnings
3-
import itertools
41
import base64
5-
import pickle
2+
import itertools
3+
import json
64
import os
5+
import pickle
6+
import re
7+
import shutil
8+
import warnings
9+
from dataclasses import dataclass
710
from typing import Any, Callable
8-
import pytest
911

12+
import pytest
1013
from testbook import testbook
14+
from testbook.client import TestbookNotebookClient
15+
16+
from tests.utils_for_qmod import qmod_compare_decorator
1117
from tests.utils_for_tests import (
18+
ROOT_DIRECTORY,
1219
resolve_notebook_path,
1320
should_skip_notebook,
14-
ROOT_DIRECTORY,
1521
)
16-
from tests.utils_for_qmod import qmod_compare_decorator
1722

1823
from classiq.interface.generator.quantum_program import QuantumProgram
1924

20-
from testbook.client import TestbookNotebookClient
21-
2225
_PATCHED = False
2326

2427

@@ -34,6 +37,7 @@
3437
3 - qmod comparison
3538
this has to come before testbook, and after cd,
3639
since it collects the qmod files before testbook, and collects again after it
40+
this is disabled (replaced by a dummy decorador: lambda x: x) in we have `replacements`
3741
3842
4 - testbook
3943
that's the main decorator
@@ -46,24 +50,33 @@
4650
adding this property must come after the testbook decorator
4751
since before the decorator, the function takes 0 arguments
4852
and after the decorator, it takes 1 - `tb`.
53+
54+
Other - replacements
55+
We allow running "regex replace" on the ipynb file, in order to ease the load on the tests.
56+
Note: adding replacements will disable the "qmod comparison" test.
4957
"""
5058

5159

52-
def wrap_testbook(notebook_name: str, timeout_seconds: float = 10) -> Callable:
60+
def wrap_testbook(
61+
notebook_name: str,
62+
timeout_seconds: float = 10,
63+
replacements: list[tuple[str, str]] | None = None,
64+
) -> Callable:
5365
def inner_decorator(func: Callable) -> Any:
5466
_patch_testbook()
5567

5668
notebook_path = resolve_notebook_path(notebook_name)
5769

58-
for decorator in [
59-
_build_patch_testbook_client_decorator(notebook_name),
60-
testbook(notebook_path, execute=True, timeout=timeout_seconds),
61-
qmod_compare_decorator,
62-
_build_cd_decorator(notebook_path),
63-
_build_skip_decorator(notebook_path),
64-
]:
65-
func = decorator(func)
66-
return func
70+
with NotebookReplace(notebook_path, replacements):
71+
for decorator in [
72+
_build_patch_testbook_client_decorator(notebook_name),
73+
testbook(notebook_path, execute=True, timeout=timeout_seconds),
74+
(lambda x: x) if replacements else qmod_compare_decorator,
75+
_build_cd_decorator(notebook_path),
76+
_build_skip_decorator(notebook_path),
77+
]:
78+
func = decorator(func)
79+
return func
6780

6881
return inner_decorator
6982

@@ -82,6 +95,82 @@ def inner(*args: Any, **kwargs: Any) -> Any:
8295
return patch_testbook_client_decorator
8396

8497

98+
FILE_COPY_SUFFIX = ".pre_test_backup"
99+
100+
101+
@dataclass
102+
class NotebookReplace:
103+
file_path: str
104+
replacements: list[tuple[str, str]] | None
105+
106+
def __post_init__(self):
107+
self.was_file_copied = False
108+
109+
@property
110+
def file_path_copied(self):
111+
return self.file_path + FILE_COPY_SUFFIX
112+
113+
def __enter__(self):
114+
if not self.replacements:
115+
return
116+
117+
self._backup_notebook()
118+
self.was_file_copied = True
119+
120+
used_replacements = self._replace_notebook_content()
121+
122+
# verify all replacements were used
123+
assert (
124+
self.replacements == used_replacements
125+
), f"Not all replacements given were used. The onces used are: {used_replacements}. The unused are {[r for r in replacements if r not in used_replacements]}"
126+
127+
def __exit__(self, *args, **kwargs):
128+
if not self.replacements:
129+
return
130+
if not self.was_file_copied:
131+
return # maybe raise?
132+
133+
self._restore_notebook_from_backup()
134+
135+
def _backup_notebook(self) -> None:
136+
assert os.path.isfile(
137+
self.file_path
138+
), f"This should not happen. '{self.file_path=}' was supposed to be a file. Aborting backup."
139+
140+
assert not os.path.exists(
141+
self.file_path_copied
142+
), f"notebook copy (for tests) was not cleaned properly. ({self.file_path_copied}). Aborting backup"
143+
shutil.copy(self.file_path, self.file_path_copied)
144+
assert os.path.exists(self.file_path_copied)
145+
146+
def _replace_notebook_content(self) -> list[tuple[str, str]]:
147+
with open(self.file_path, "r") as f:
148+
content = f.read()
149+
150+
used_replacements = []
151+
152+
for pattern, replace in self.replacements:
153+
new_content = re.sub(pattern, replace, content)
154+
if new_content != content:
155+
used_replacements.append((pattern, replace))
156+
content = new_content
157+
158+
# write edited content
159+
with open(self.file_path, "w") as f:
160+
f.write(content)
161+
162+
return used_replacements
163+
164+
def _restore_notebook_from_backup(self) -> None:
165+
assert os.path.isfile(
166+
self.file_path_copied
167+
), f"This should not happen. '{self.file_path_copied=}' was supposed to be a file. Aborting restore."
168+
shutil.move(self.file_path_copied, self.file_path)
169+
assert not os.path.exists(
170+
self.file_path_copied
171+
), f"notebook copy (for tests) was not cleaned properly. ({self.file_path_copied}). Aborting restore."
172+
173+
85174
# The purpose of the `cd_decorator` is to execute the test in the same folder as the `ipynb` file
86175
# so that relative files (images, csv, etc.) will be available
87176
def _build_cd_decorator(file_path: str) -> Callable:

0 commit comments

Comments
 (0)