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

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 

6 

7"""AI: Recipe that converts taut-style test files to Python's unittest style.""" 

8 

9import re 

10import textwrap 

11from datetime import datetime 

12from pathlib import Path 

13 

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 

19 

20 

21class TautToPythonUnittest(PythonRefactoring): 

22 """AI: Recipe that converts taut-style test files to Python's unittest style.""" 

23 

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" 

30 

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()}") 

40 

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() 

48 

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() 

56 

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() 

71 

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() 

75 

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.") 

82 

83 def get_migrated_path(self, file_path): 

84 """Convert a file path to add '_migrated' before the extension. 

85 

86 Example: 'taut.py' -> 'taut_migrated.py' 

87 """ 

88 path = Path(file_path) 

89 return path.with_stem(f"{path.stem}_migrated") 

90 

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 ] 

103 

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"] 

107 

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 ] 

135 

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 ] 

142 

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 ] 

160 

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"] 

164 

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 ] 

172 

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) 

179 

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() 

192 

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) 

198 

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) 

204 

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) 

216 

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) 

223 

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) 

228 

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 = {} 

235 

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) 

257 

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) 

270 

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) 

278 

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] 

290 

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) 

298 

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) 

302 

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))") 

306 

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) 

325 

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) 

340 

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 ] 

350 

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) 

359 

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) 

367 

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") 

372 

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) 

377 

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) 

393 

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) 

402 

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"] 

406 

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) 

413 

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) 

420 

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) 

427 

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) 

436 

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) 

443 

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() 

450 

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() 

459 

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 ] 

471 

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) 

486 

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() 

526 

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() 

560 

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) 

624 

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): 

628 

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 

646 

647 def tearDown(self): 

648 $$gg 

649 for double in self.doubles: 

650 double.exit()""") 

651 

652 for match in match_pattern(self.root.children, pattern): 

653 replace_pattern = f"""class {match["$a"]}(unittest.TestCase): 

654 

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() 

664 

665{textwrap.indent(match["$$dd"], " ")} 

666 

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) 

672 

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) 

682 

683 

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) 

688 

689 if not match: 

690 print("Comment block not found.") 

691 return content 

692 

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 

698 

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 

703 

704 

705def get_change_comment(date=None) -> str: 

706 """Generate a formatted change comment with today's date. 

707 

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. 

710 

711 Returns: 

712 str: Formatted change comment string 

713 

714 """ 

715 change_id = "SWCHGxxxxxxxx" 

716 description = "Add assert_raises method to Asserter class." 

717 

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) 

721 

722 return f"# {formatted_date.strftime('%m-%d-%Y')} : {change_id} SBYN {description}"