Coverage for src/renaissance/recipes/taut_to_python_unittest.py: 84%
353 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Recipe that converts taut-style test files to Python's unittest style."""
9import re
10import textwrap
11from datetime import datetime
12from pathlib import Path
14import test_data.test_class as tst_class
15import test_data.test_insert as tst_insert
16from renaissance.recipes.python_refactoring import PythonRefactoring
17from renaissance.syntax_tree.match_finder import match_pattern
18from renaissance.syntax_tree.semantic_kind import SemanticKind
21class TautToPythonUnittest(PythonRefactoring):
22 """AI: Recipe that converts taut-style test files to Python's unittest style."""
24 def __init__(self, file) -> None:
25 """AI: Prepare a refactoring processor that converts taut-style tests to Python unittest style."""
26 super().__init__(file)
27 self.white_list_reg = r"_test|_unittest|_tests"
28 self.black_list_reg = r"_migrated|_after|_original"
29 self.comp = "ABCD"
31 def run(self) -> None:
32 """AI: Apply the full sequence of taut-to-unittest migration transforms and write the migrated file."""
33 if re.search(self.black_list_reg, self.filename):
34 print(f"skipping: {Path(self.filename).resolve()}")
35 return
36 if not re.search(self.white_list_reg, self.filename):
37 print(f"skipping: {Path(self.filename).resolve()}")
38 return
39 print(f"Taut to pyunit migration: {Path(self.filename).resolve()}")
41 # conditional refactor
42 if "AP_core_functionality_test" in self.filename:
43 self.insert_asserter()
44 self.remove_assert_func()
45 self.replace_unittest_with_asserter()
46 self.assert_func()
47 self.commit()
49 self.replace_mock()
50 self.remove_stubserver()
51 self.replace_taut()
52 self.remove_decorator()
53 self.add_self()
54 self.convert_assert()
55 self.convert_testdoubles_fun()
57 self.replace_log_compxtl("emrw")
58 self.replace_log_compxtl("abcd")
59 self.remove_taut_import()
60 self.replace_taut_import()
61 self.convert_setup_common()
62 self.convert_teardown_common()
63 self.convert_add_patcher()
64 self.convert_teardown()
65 self.convert_setup()
66 self.convert_import_verify()
67 self.shared_setup()
68 self.commit()
69 self.with_testdoubles()
70 self.commit()
72 if self.root.signature.find("self.patches = []") > 0 or self.root.signature.find("patch.object") > 0:
73 self.insert_patch_import()
74 self.commit()
76 try:
77 # result = insert_doc(result, "01-22-2026")
78 with self.get_migrated_path(self.filename).open("w") as f:
79 f.write(self.apply_to_string())
80 except FileNotFoundError:
81 print(f"Error: File '{self.filename}' not found.")
83 def get_migrated_path(self, file_path):
84 """Convert a file path to add '_migrated' before the extension.
86 Example: 'taut.py' -> 'taut_migrated.py'
87 """
88 path = Path(file_path)
89 return path.with_stem(f"{path.stem}_migrated")
91 def replace_taut(self) -> None:
92 """Replace TAUT.TestCase by unittest.TestCase."""
93 [
94 self.replace("unittest.TestCase", node, False, False)
95 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
96 if node.name == "TAUT.TestCase"
97 ]
98 [
99 self.replace("unittest.TestCase", node, False, False)
100 for node in self.find_semantic_kind(SemanticKind.NAME)
101 if node.name == "TestCase"
102 ]
104 def remove_decorator(self) -> None:
105 """AI: Remove the @TAUT.log_stub decorator."""
106 [self.remove(node, False, False) for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE) if node.name == "TAUT.log_stub"]
108 def add_self(self) -> None:
109 """AI: Prefix known TAUT helper attribute names with "self." so they resolve as instance members."""
110 matching = [
111 "emrwxread",
112 "emrwxwidxread",
113 "emrwxviprxinterface",
114 "whxstream2",
115 "gtaaxtxmark",
116 "mark_upd_q",
117 "gtaaxtxmark",
118 "gtaaxtxmrkxadv",
119 "emrwxwidxcfg",
120 "wlxload",
121 "wlxclear",
122 "gtmwxtxws",
123 "emtlxt",
124 "emtlxtxmc",
125 "emtlxtxwid",
126 "emrwxviprxtestlog",
127 "emrwxviprxwh",
128 ]
129 parent_func = ["setUpCommon", "setUp"]
130 [
131 self.replace("self." + node.name, node, False, False)
132 for node in self.find_semantic_kind(SemanticKind.NAME)
133 if node.name in matching
134 ]
136 matching2 = ["EMRWxREAD.emrwxread"]
137 [
138 self.replace("self." + node.name.split(".")[1], node, False, False)
139 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
140 if node.name in matching2 and node.get_ancestor("FunctionDef").name not in parent_func
141 ]
143 def convert_assert(self) -> None:
144 """AI: Replace TAUT-style assert_false/assert_true/assert_equal calls with unittest equivalents."""
145 [
146 self.replace("self.assertFalse", node, False, False)
147 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
148 if node.name == "self.assert_false"
149 ]
150 [
151 self.replace("self.assertTrue", node, False, False)
152 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
153 if node.name == "self.assert_true"
154 ]
155 [
156 self.replace("self.assertEqual", node, False, False)
157 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
158 if node.name == "self.assert_equal"
159 ]
161 def remove_stubserver(self) -> None:
162 """AI: Remove references to TAUT.StubServer."""
163 [self.remove(node, False, False) for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE) if node.name == "TAUT.StubServer"]
165 def replace_mock(self) -> None:
166 """AI: Replace mock.patch decorators with patch."""
167 [
168 self.replace("patch", node, False, False)
169 for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE)
170 if node.name == "mock.patch" and node.parent.parent.name == "decorator_list"
171 ]
173 def replace_log_compxtl(self, comp) -> None:
174 """AI: Replace direct and assigned calls to {comp}xtl with a fake_{comp}xtl double, including TestDoubles blocks."""
175 func_call = self.pattern_factory.create_statements(f"{comp}xtl.$a($$bb)")
176 for call in match_pattern(self.root.children, func_call):
177 repl = call.signature.replace(f"{comp}xtl", f"fake_{comp}xtl")
178 self.replace(repl, call.nodes, False, False)
180 assign = self.pattern_factory.create_statements(f"$c = {comp}xtl.$a($$bb)")
181 for match in match_pattern(self.root.children, assign):
182 repl = match.signature.replace(f"{comp}xtl", f"fake_{comp}xtl")
183 self.replace(repl, match.nodes, False, False)
184 self.commit()
185 taut_test_doubles = self.pattern_factory.create_statements(
186 f"with TAUT.TestDoubles({comp}xtl=Fake{comp.upper()}xTL(None)):\n log = TAUT.Logger()\n $$aa",
187 )
188 for match in match_pattern(self.root.children, taut_test_doubles):
189 repl = f"fake_{comp}xtl = Fake{comp.upper()}xTL(None)\n{match['$$aa']}"
190 self.replace(repl, match.nodes, False, False)
191 self.commit()
193 def remove_taut_import(self) -> None:
194 """AI: Remove the "import TAUT" statement."""
195 taut_import = self.pattern_factory.create_statements("import TAUT\n")
196 for match in match_pattern(self.root.children, taut_import):
197 self.remove(match.nodes, False, False)
199 def replace_taut_import(self) -> None:
200 """Replace mock by unittest.mock and using patch."""
201 mock = self.pattern_factory.create_statements("import mock\n")
202 for match in match_pattern(self.root.children, mock):
203 self.remove(match.nodes, False, False)
205 test_case = self.pattern_factory.create_statements("from TAUT import TestCase")
206 for match in match_pattern(self.root.children, test_case):
207 self.remove(match.nodes, False, False)
208 import_taut = self.pattern_factory.create_statements("from TAUT import TestCase, TestDoubles")
209 for match in match_pattern(self.root.children, import_taut):
210 repl = "try:\n from unittest.mock import patch\nexcept ImportError:\n from mock import patch\n"
211 self.replace(repl, match.nodes, False, False)
212 import_doubles = self.pattern_factory.create_statements("from TAUT import TestDoubles")
213 for match in match_pattern(self.root.children, import_doubles):
214 repl = "try:\n from unittest.mock import patch\nexcept ImportError:\n from mock import patch\n"
215 self.replace(repl, match.nodes, False, False)
217 def convert_tds(self) -> None:
218 """AI: Convert self.tds.append(TestDoubles(...)) calls into add_patcher calls or direct stub assignments."""
219 tds = self.pattern_factory.create_statements("self.tds.append(TestDoubles($a, $b=$c))")
220 for match in match_pattern(self.root.children, tds):
221 repl = f"self.add_patcher({match['$a']}, '{match['$b']}', {match['$c']})"
222 self.replace(repl, match.nodes, False, False)
224 tds2 = self.pattern_factory.create_statements("self.tds.append(TestDoubles($a=ImprovedStub($b)))")
225 for match in match_pattern(self.root.children, tds2):
226 repl = f"self.{match['$a']} = ImprovedStub({match['$b']})"
227 self.replace(repl, match.nodes, False, False)
229 def convert_setup_common(self) -> None:
230 """AI: Convert self.tds = [...] blocks into ImprovedStub setup plus patch.object patcher lists."""
231 insert_code = """ImprovedStub.ret_vals = {}
232ImprovedStub.ret_vals_ex = {}
233ImprovedStub.call_logs = {}
234ImprovedStub.store_args = {}
236"""
237 p_start = """for p in self.patchers:
238 p.start()
239"""
240 tds_pattern = self.pattern_factory.create_statements("self.tds = [$$aa]")
241 for match in match_pattern(self.root.children, tds_pattern):
242 init_stubs = ""
243 repl = "self.patchers = [\n"
244 doubles_pattern = self.pattern_factory.create_expression("TestDoubles($a=ImprovedStub($b))")
245 for matched_doubles in match_pattern(match.expansions["$$aa"], [doubles_pattern]):
246 init_stubs += (
247 f"self.{matched_doubles.expansions['$a'][0]} = ImprovedStub({matched_doubles.expansions['$b'][0].signature})\n"
248 )
249 interface_stub = self.find_import_interface(matched_doubles.expansions["$b"][0].signature)
250 repl += (
251 f" patch.object({interface_stub}, '{matched_doubles.expansions['$a'][0]}', "
252 f"self.{matched_doubles.expansions['$a'][0]}),\n"
253 )
254 repl += "]\n\n"
255 repl = insert_code + init_stubs + repl + p_start
256 self.replace(repl, match.nodes, False, False)
258 def convert_teardown_common(self) -> None:
259 """AI: Replace tearDownCommon's body with logic that stops all patchers."""
260 teardown_common = self.pattern_factory.create_statements("def tearDownCommon(self):\n $$aa")
261 repl = """def tearDownCommon(self):
262 for p in self.patchers:
263 try:
264 p.stop()
265 except RuntimeError:
266 pass
267"""
268 for match in match_pattern(self.root.children, teardown_common):
269 self.replace(repl, match.nodes, False, False)
271 def convert_add_patcher(self) -> None:
272 """AI: Insert an add_patcher helper method into tearDownCommon if one doesn't already exist."""
273 pattern = self.pattern_factory.create_statements("def tearDownCommon(self):\n $$aa")
274 for match in match_pattern(self.root.children, pattern):
275 patcher_pattern = [node for node in self.find_semantic_kind(SemanticKind.FUNCTION) if node.name == "add_patcher"]
276 if len(patcher_pattern) == 0:
277 self.insert_after(tst_class.insert_add_patcher, match.nodes)
279 def find_import_interface(self, name: str):
280 """AI: Return the top-level module/interface name that name was imported from, or name itself."""
281 interface = name
282 if name.islower():
283 node_list = [node for node in self.find_semantic_kind(SemanticKind.IMPORT) if node.name == name]
284 if node_list:
285 if node_list[0].parser_kind == "ImportFrom":
286 interface = node_list[0].properties["module"]
287 else:
288 interface = node_list[0].name if node_list else name
289 return interface.split(".")[0]
291 def convert_setup(self) -> None:
292 """AI: Convert TAUT.TestDoubles-based setUp logic into patch/patch.object based setup with self.patches."""
293 # remove doubles init
294 pattern1 = self.pattern_factory.create_statements("doubles = []")
295 replacement = "self.patches = []"
296 for match in match_pattern(self.root.children, pattern1):
297 self.replace(replacement, match.nodes, False, False)
299 pattern2 = self.pattern_factory.create_statements("self.doubles = []")
300 for match in match_pattern(self.root.children, pattern2):
301 self.replace(replacement, match.nodes, False, False)
303 # convert doubles to patch
304 self.convert_test_doubles("doubles.append(TAUT.TestDoubles($a=$b))")
305 self.convert_test_doubles("self.doubles.append(TAUT.TestDoubles($a=$b))")
307 # convert doubles to patch.object
308 insert_node = None
309 pattern_outer = self.pattern_factory.create_statements("def setUp(self):\n $$aa")
310 for setup_func in match_pattern(self.root.children, pattern_outer):
311 pattern4 = self.pattern_factory.create_statements("doubles.append(TAUT.TestDoubles(module=$mod, $b=$c))")
312 matched_pattern = match_pattern(setup_func.nodes, pattern4)
313 for index, match in enumerate(matched_pattern):
314 repl_pattern = (
315 f"self.patches.append(patch.object({match.expansions['$mod'][0].name}, "
316 f"'{match.expansions['$b'][0]}', {match.expansions['$c'][0].signature}))"
317 )
318 repl_pattern = repl_pattern.replace("context_stub", "self.context_stub")
319 self.replace(repl_pattern, match.nodes, False, False)
320 if index == len(matched_pattern) - 1:
321 insert_node = match.nodes[-1]
322 insert_code = """\nfor p in self.patches:
323 p.start()"""
324 self.insert_after(insert_code, insert_node, False, False)
326 pattern4_1 = self.pattern_factory.create_statements("self.doubles.append(TAUT.TestDoubles(module=$mod, $b=$c))")
327 matched_pattern_1 = match_pattern(setup_func.nodes, pattern4_1)
328 for index, match in enumerate(matched_pattern_1):
329 repl_pattern = (
330 f"self.patches.append(patch.object({match.expansions['$mod'][0].name}, "
331 f"'{match.expansions['$b'][0]}', {match.expansions['$c'][0].signature}))"
332 )
333 repl_pattern = repl_pattern.replace("context_stub", "self.context_stub")
334 self.replace(repl_pattern, match.nodes, False, False)
335 if index == len(matched_pattern_1) - 1:
336 insert_node = match.nodes[-1]
337 insert_code = """\nfor p in self.patches:
338 p.start()"""
339 self.insert_after(insert_code, insert_node, False, False)
341 pattern5 = self.pattern_factory.create_statements("self.doubles = doubles")
342 for match in match_pattern(self.root.children, pattern5):
343 self.remove(match.nodes, False, False)
344 self.commit()
345 [
346 self.replace("self.context_stub", node, False, False)
347 for node in self.find_semantic_kind(SemanticKind.NAME)
348 if node.name == "context_stub"
349 ]
351 def convert_teardown(self) -> None:
352 """AI: Replace tearDown's body with logic that stops all patches."""
353 matched_pattern = self.pattern_factory.create_statements("def tearDown(self):\n $$aa")
354 repl_pattern = """def tearDown(self):
355 for p in self.patches:
356 p.stop()"""
357 for match in match_pattern(self.root.children, matched_pattern):
358 self.replace(repl_pattern, match.nodes, False, False)
360 def refactor_teardown(self) -> None:
361 """AI: Replace the doubles-exit teardown loop with patch.stopall() and reset context method attributes."""
362 self.comp = "abcd"
363 pattern1 = self.pattern_factory.create_statements("for double in self.doubles:\n double.exit()")
364 replace_pattern = "patch.stopall()"
365 for match in match_pattern(self.root.children, pattern1):
366 self.replace(replace_pattern, match.nodes, False, False)
368 insert_code = f"""{self.comp.upper()}xCONTEXT.{self.comp}xcontext.reset_method_attributes("start_wafer")
369{self.comp.upper()}xCONTEXT.{self.comp}xcontext.reset_method_attributes("finish_wafer")
370{self.comp.upper()}xCONTEXT.{self.comp}xcontext.reset_method_attributes("start_lot")
371{self.comp.upper()}xCONTEXT.{self.comp}xcontext.reset_method_attributes("finish_lot")
373"""
374 pattern2 = self.pattern_factory.create_statements("self._patch_readout_data_filler.stop()")
375 for match in match_pattern(self.root.children, pattern2):
376 self.insert_before(insert_code, match.nodes, False, False)
378 def convert_test_doubles(self, doubles: str) -> None:
379 """AI: Convert matches of the given doubles-append pattern into self.patches.append(patch(...)) calls."""
380 mappings: dict[str, str] = {
381 "emrmxcontext": "EMRMxCONTEXT",
382 "acbdxcontext": "ACBDxCONTEXT",
383 # Add more mappings here
384 }
385 doubles_pattern = self.pattern_factory.create_statements(doubles)
386 for match in match_pattern(self.root.children, doubles_pattern):
387 keyword = match.expansions["$a"][0]
388 if match.expansions["$a"][0] in mappings:
389 keyword = mappings[match.expansions["$a"][0]]
390 repl_pattern = f"self.patches.append(patch('{keyword}.{match.expansions['$a'][0]}', {match.expansions['$b'][0].name}))"
391 repl_pattern = repl_pattern.replace("context_stub", "self.context_stub")
392 self.replace(repl_pattern, match.nodes, False, False)
394 def insert_patch_import(self) -> None:
395 """AI: Insert a try/except import of unittest.mock.patch (falling back to mock.patch) if missing."""
396 insert = "\ntry:\n from unittest.mock import patch\nexcept ImportError:\n from mock import patch"
397 insert_pattern = self.pattern_factory.create_statements(insert)
398 if len(match_pattern(self.root.children, insert_pattern)) == 0:
399 pattern = self.pattern_factory.create_statements("import unittest\n")
400 for match in match_pattern(self.root.children, pattern):
401 self.insert_after(insert, match.nodes, False, False)
403 def replace_taut_skip(self) -> None:
404 """Replace @TAUT.skip_test by @unittest.skip."""
405 [self.replace("@unittest.skip", node) for node in self.find_semantic_kind(SemanticKind.ATTRIBUTE) if node.name == "TAUT.skip_test"]
407 def convert_import_verify(self) -> None:
408 """AI: Replace self.import_and_verify_module('$a') calls with a real import plus an assertIsNotNone check."""
409 import_verify = self.pattern_factory.create_statements("self.import_and_verify_module('$a')")
410 for match in match_pattern(self.root.children, import_verify):
411 repl = f"import {match.expansions['$a'][0]}\nself.assertIsNotNone({match.expansions['$a'][0]})"
412 self.replace(repl, match.nodes, False, False)
414 def with_testdoubles(self) -> None:
415 """AI: Replace "with TAUT.TestDoubles(...)" blocks with equivalent "with patch.object(...)" blocks."""
416 pattern1 = self.pattern_factory.create_statements("with TAUT.TestDoubles(module=$a, $b=$c):\n $$ee")
417 for match in match_pattern(self.root.children, pattern1):
418 repl_pattern = f"with patch.object({match['$a']}, '{match['$b']}', new={match['$c']}):\n {match['$$ee']}"
419 self.replace(repl_pattern, match.nodes, False, False)
421 def shared_setup(self) -> None:
422 """AI: Rename a sharedSetUp method to setUp."""
423 setup_function = self.pattern_factory.create_statements("def sharedSetUp(self):\n $$stmts")
424 for match in match_pattern(self.root.children, setup_function):
425 repl = match.signature.replace("def sharedSetUp", " def setUp")
426 self.replace(textwrap.dedent(repl), match.nodes, False, False)
428 def insert_class(self) -> None:
429 """AI: Insert the Asserter helper class if it doesn't already exist in the file."""
430 class_pattern = self.pattern_factory.create_statements("class Asserter(unittest.TestCase):\n $$aa")
431 if len(match_pattern(self.root.children, class_pattern)) == 0:
432 insert_pattern = self.pattern_factory.create_statements("def b():\n $$bb")
433 insert_code = tst_insert.insert_code
434 for match in match_pattern(self.root.children, insert_pattern):
435 self.insert_after(insert_code, match.nodes, False, False)
437 def insert_asserter(self) -> None:
438 """AI: Insert the assert_double_equal helper function's replacement code."""
439 insert_pattern = self.pattern_factory.create_statements("def assert_double_equal($$arg, $$other=$$value):\n $$bb")
440 insert_code = tst_insert.insert_code
441 for match in match_pattern(self.root.children, insert_pattern):
442 self.insert_after(insert_code, match.nodes, False, False)
444 def remove_assert_func(self) -> None:
445 """AI: Remove the original assert_double_equal function definition."""
446 pattern = self.pattern_factory.create_statements("def assert_double_equal($$arg, $$other=$$value):\n $$bb")
447 for match in match_pattern(self.root.children, pattern):
448 self.remove(match.nodes, False, False)
449 self.commit()
451 def replace_unittest_with_asserter(self) -> None:
452 """AI: Make classes that use assert_raises/assert_double_equal extend Asserter instead of TAUT.TestCase."""
453 pattern = self.pattern_factory.create_statements("class $a(TAUT.TestCase):\n $$bb")
454 for match in match_pattern(self.root.children, pattern):
455 if match["$a"] != "Asserter" and ("assert_raises" in match["$$bb"] or "assert_double_equal" in match["$$bb"]):
456 repl = f"{match.signature.replace('TAUT.TestCase', 'Asserter')}"
457 self.replace(repl, match.nodes, False, False)
458 self.commit()
460 def assert_func(self) -> None:
461 """AI: Prefix assert_raises and assert_double_equal usages with "self."."""
462 matching = [
463 "assert_raises",
464 "assert_double_equal",
465 ]
466 [
467 self.replace("self." + node.name, node, False, False)
468 for node in self.find_semantic_kind(SemanticKind.NAME)
469 if node.name in matching
470 ]
472 def move_indent(self, indent) -> None:
473 """AI: Replace a doubles-append call inside a function with an indented patch.object with-block."""
474 pattern1 = self.pattern_factory.create_statements("""def $a($$b):
475 self.doubles.append(TAUT.TestDoubles($mod, $e, $f))
476 $$c""")
477 for match in match_pattern(self.root.children, pattern1):
478 double_pattern = f" self.doubles.append(TAUT.TestDoubles({match['$mod']}, {match['$e']}, {match['$f']}))\n"
479 func_header_index = match.signature.index("):\n")
480 repl = f""" with patch.object({match["$mod"]}, '{match["$e"]}', {match["$f"]}):\n"""
481 replace_pattern = (
482 match.signature[: func_header_index + 3] + repl + textwrap.indent(match.signature[func_header_index + 3 :], indent)
483 )
484 replace_pattern = replace_pattern.replace(double_pattern, "")
485 self.replace(replace_pattern, match.nodes, False, False)
487 def convert_testdoubles_fun(self) -> None:
488 """AI: Use this for taut migration, where the function pattern is found in a class."""
489 # case1 two TestDoubles are defined
490 pattern1 = self.pattern_factory.create_statements("""def $a($$b):
491 self.doubles.append(
492 TAUT.TestDoubles(
493 module=$mod1, $e1=$f1
494 )
495 )
496 self.doubles.append(
497 TAUT.TestDoubles(
498 module=$mod2, $e2=$f2
499 )
500 )
501 $$c
502 """)
503 for match in match_pattern(self.root.children, pattern1):
504 double_pattern = f""" self.doubles.append(
505 TAUT.TestDoubles(
506 module={match["$mod1"]},
507 {match["$e1"]}={match["$f1"]},
508 ),
509 )
510 self.doubles.append(
511 TAUT.TestDoubles(
512 module={match["$mod2"]},
513 {match["$e2"]}={match["$f2"]},
514 ),
515 )
516"""
517 func_header_index = match.signature.index("):\n")
518 repl = f"""with patch.object({match["$mod1"]}, '{match["$e1"]}', {match["$f1"]}), \\
519 patch.object({match["$mod2"]}, '{match["$e2"]}', {match["$f2"]}):\n"""
520 replace_pattern = (
521 match.signature[: func_header_index + 3] + textwrap.indent(repl, " ") + match.signature[func_header_index + 3 :]
522 )
523 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern, " "), "")
524 self.replace(replace_pattern, match.nodes, False, False)
525 self.commit()
527 pattern2 = self.pattern_factory.create_statements("""def $a($$b):
528 self.doubles.append(
529 TAUT.TestDoubles(
530 module=$mod, $e=$f
531 )
532 )
533 $$c
534 """)
535 for match in match_pattern(self.root.children, pattern2):
536 double_pattern = f""" self.doubles.append(
537 TAUT.TestDoubles(
538 module={match["$mod"]}, {match["$e"]}={match["$f"]}
539 )
540 )
541"""
542 double_pattern1 = f""" self.doubles.append(
543 TAUT.TestDoubles(
544 module={match["$mod"]},
545 {match["$e"]}={match["$f"]},
546 )
547 )
548"""
549 func_header_index = match.signature.index("):\n")
550 repl = f"""with patch.object({match["$mod"]}, '{match["$e"]}', {match["$f"]}):\n"""
551 replace_pattern = (
552 match.signature[: func_header_index + 3] + textwrap.indent(repl, " ") + match.signature[func_header_index + 3 :]
553 )
554 double_pattern2 = f" self.doubles.append(TAUT.TestDoubles(module={match['$mod']}, {match['$e']}={match['$f']}))\n"
555 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern, " "), "")
556 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern1, " "), "")
557 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern2, " "), "")
558 self.replace(replace_pattern, match.nodes, False, False)
559 self.commit()
561 def refactor_testdoubles_fun(self) -> None:
562 """Use this for unittest, where the function pattern is not found in a class."""
563 # case1 two TestDoubles are defined
564 pattern1 = self.pattern_factory.create_statements("""def $a($$b):
565 self.doubles.append(
566 TAUT.TestDoubles(
567 module=$mod1, $e1=$f1
568 )
569 )
570 self.doubles.append(
571 TAUT.TestDoubles(
572 module=$mod2, $e2=$f2
573 )
574 )
575 $$c""")
576 for match in match_pattern(self.root.children, pattern1):
577 double_pattern = f""" self.doubles.append(
578 TAUT.TestDoubles(
579 module={match["$mod1"]}, {match["$e1"]}={match["$f1"]}
580 )
581 )
582 self.doubles.append(
583 TAUT.TestDoubles(
584 module={match["$mod2"]}, {match["$e2"]}={match["$f2"]}
585 )
586 )
587"""
588 func_header_index = match.signature.index("):\n")
589 repl = f"""with patch.object({match["$mod1"]}, '{match["$e1"]}', {match["$f1"]}), \\
590 patch.object({match["$mod2"]}, '{match["$e2"]}', {match["$f2"]}):
591 """
592 replace_pattern = match.signature[: func_header_index + 3] + textwrap.indent(
593 repl + match.signature[func_header_index + 3 :],
594 " ",
595 )
596 replace_pattern = replace_pattern.replace(double_pattern, "")
597 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern, " "), "")
598 self.replace(replace_pattern, match.nodes, False, False)
599 self.commit()
600 pattern2 = self.pattern_factory.create_statements("""def $a($$b):
601 self.doubles.append(
602 TAUT.TestDoubles(
603 module=$mod, $e=$f
604 )
605 )
606 $$c
607""")
608 for match in match_pattern(self.root.children, pattern2):
609 double_pattern = f""" self.doubles.append(
610 TAUT.TestDoubles(
611 module={match["$mod"]}, {match["$e"]}={match["$f"]}
612 )
613 )
614"""
615 func_header_index = match.signature.index("):\n")
616 repl = f"""with patch.object({match["$mod"]}, '{match["$e"]}', {match["$f"]}):\n"""
617 replace_pattern = match.signature[: func_header_index + 3] + textwrap.indent(
618 repl + match.signature[func_header_index + 3 :],
619 " ",
620 )
621 replace_pattern = replace_pattern.replace(double_pattern, "")
622 replace_pattern = replace_pattern.replace(textwrap.indent(double_pattern, " "), "")
623 self.replace(replace_pattern, match.nodes, False, False)
625 def refactor_testdoubles_class(self) -> None:
626 """AI: Convert a class-level TAUT.TestDoubles-based setUp/tearDown into patch.object based setup/teardown."""
627 pattern = self.pattern_factory.create_statements("""class $a(TAUT.TestCase):
629 def setUp(self):
630 $$bb
631 self.doubles = []
632 $$cc
633 self.doubles.append(
634 TAUT.TestDoubles(
635 module=$mod1,
636 $e1=$f1,
637 )
638 )
639 self.doubles.append(
640 TAUT.TestDoubles(
641 module=$mod2,
642 $e2=$f2,
643 )
644 )
645 $$dd
647 def tearDown(self):
648 $$gg
649 for double in self.doubles:
650 double.exit()""")
652 for match in match_pattern(self.root.children, pattern):
653 replace_pattern = f"""class {match["$a"]}(unittest.TestCase):
655 def setUp(self):
656{textwrap.indent(match["$$bb"], " ")}
657{textwrap.indent(match["$$cc"], " ")}
658 self.patches = [
659 patch.object({match["$mod1"]}, '{match["$e1"]}', {match["$f1"]}),
660 patch.object({match["$mod2"]}, '{match["$e2"]}', {match["$f2"]}),
661 ]
662 for p in self.patches:
663 p.start()
665{textwrap.indent(match["$$dd"], " ")}
667 def tearDown(self):
668{textwrap.indent(match["$$gg"], " ")}
669 for p in self.patches:
670 p.stop()"""
671 self.replace(replace_pattern, match.nodes, False, False)
673 def insert_doc_func(self) -> None:
674 """AI: Insert a change-log comment before the existing copyright comment block."""
675 pattern = self.pattern_factory.create_statements("""# -----------------------------------------------------------------------------#
676# #
677# Copyright (c) 2016, XXXX Netherlands B.V. #
678""")
679 insert_code = get_change_comment()
680 for match in match_pattern(self.root.children, pattern):
681 self.insert_before(insert_code, match.nodes, False, False)
684def insert_doc(content: str, date):
685 """AI: Insert a change-log comment line just before the existing copyright comment block in content."""
686 pattern = r"# -+(#)?\n(#\s+#\n)?#\s+Copyright \(c\) \d{4}, XXXX"
687 match = re.search(pattern, content)
689 if not match:
690 print("Comment block not found.")
691 return content
693 # Find the beginning of the line containing the comment
694 position = match.start()
695 line_start = content.rfind("\n", 0, position) + 1
696 if line_start == 0: # If comment is at the beginning of the file
697 line_start = 0
699 # Insert the new line before the comment block
700 print(get_change_comment(date))
701 modified_content = content[:line_start] + get_change_comment(date) + "\n" + content[line_start:]
702 return modified_content
705def get_change_comment(date=None) -> str:
706 """Generate a formatted change comment with today's date.
708 Args:
709 date (str, optional): The date to use for the change comment, in "%m-%d-%Y" format. Defaults to today's date if None.
711 Returns:
712 str: Formatted change comment string
714 """
715 change_id = "SWCHGxxxxxxxx"
716 description = "Add assert_raises method to Asserter class."
718 # When no date provided, use today
719 tz = datetime.UTC
720 formatted_date = datetime.now(tz) if date is None else datetime.strptime(date, "%m-%d-%Y").astimezone(tz)
722 return f"# {formatted_date.strftime('%m-%d-%Y')} : {change_id} SBYN {description}"