1- import json
2- import warnings
3- import itertools
41import base64
5- import pickle
2+ import itertools
3+ import json
64import os
5+ import pickle
6+ import re
7+ import shutil
8+ import warnings
9+ from dataclasses import dataclass
710from typing import Any , Callable
8- import pytest
911
12+ import pytest
1013from testbook import testbook
14+ from testbook .client import TestbookNotebookClient
15+
16+ from tests .utils_for_qmod import qmod_compare_decorator
1117from 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
1823from classiq .interface .generator .quantum_program import QuantumProgram
1924
20- from testbook .client import TestbookNotebookClient
21-
2225_PATCHED = False
2326
2427
34373 - qmod comparison
3538this 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
38424 - testbook
3943that's the main decorator
4650adding this property must come after the testbook decorator
4751since before the decorator, the function takes 0 arguments
4852and 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
87176def _build_cd_decorator (file_path : str ) -> Callable :
0 commit comments