test_agent_skill.py 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633
  1. import contextlib
  2. import io
  3. import os
  4. import sys
  5. from unittest.mock import patch
  6. import docx
  7. import pytest
  8. from openhands.runtime.plugins.agent_skills.file_ops.file_ops import (
  9. MSG_FILE_UPDATED,
  10. WINDOW,
  11. _print_window,
  12. append_file,
  13. create_file,
  14. edit_file_by_replace,
  15. find_file,
  16. goto_line,
  17. insert_content_at_line,
  18. open_file,
  19. scroll_down,
  20. scroll_up,
  21. search_dir,
  22. search_file,
  23. )
  24. from openhands.runtime.plugins.agent_skills.file_reader.file_readers import (
  25. parse_docx,
  26. parse_latex,
  27. parse_pdf,
  28. parse_pptx,
  29. )
  30. from openhands.runtime.plugins.agent_skills.utils.aider import Linter
  31. # CURRENT_FILE must be reset for each test
  32. @pytest.fixture(autouse=True)
  33. def reset_current_file():
  34. from openhands.runtime.plugins.agent_skills import agentskills
  35. agentskills.CURRENT_FILE = None
  36. def _numbered_test_lines(start, end) -> str:
  37. return ('\n'.join(f'{i}|' for i in range(start, end + 1))) + '\n'
  38. def _generate_test_file_with_lines(temp_path, num_lines) -> str:
  39. file_path = temp_path / 'test_file.py'
  40. file_path.write_text('\n' * num_lines)
  41. return file_path
  42. def _generate_ruby_test_file_with_lines(temp_path, num_lines) -> str:
  43. file_path = temp_path / 'test_file.rb'
  44. file_path.write_text('\n' * num_lines)
  45. return file_path
  46. def _calculate_window_bounds(current_line, total_lines, window_size):
  47. """Calculate the bounds of the window around the current line."""
  48. half_window = window_size // 2
  49. if current_line - half_window < 0:
  50. start = 1
  51. end = window_size
  52. else:
  53. start = current_line - half_window
  54. end = current_line + half_window
  55. return start, end
  56. def _generate_ruby_test_file_with_lines(temp_path, num_lines) -> str:
  57. file_path = temp_path / 'test_file.rb'
  58. file_path.write_text('\n' * num_lines)
  59. return file_path
  60. def _capture_file_operation_error(operation, expected_error_msg):
  61. with io.StringIO() as buf:
  62. with contextlib.redirect_stdout(buf):
  63. operation()
  64. result = buf.getvalue().strip()
  65. assert result == expected_error_msg
  66. SEP = '-' * 49 + '\n'
  67. # =============================================================================
  68. def test_open_file_unexist_path():
  69. _capture_file_operation_error(
  70. lambda: open_file('/unexist/path/a.txt'),
  71. 'ERROR: File /unexist/path/a.txt not found.',
  72. )
  73. def test_open_file(tmp_path):
  74. assert tmp_path is not None
  75. temp_file_path = tmp_path / 'a.txt'
  76. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  77. with io.StringIO() as buf:
  78. with contextlib.redirect_stdout(buf):
  79. open_file(str(temp_file_path))
  80. result = buf.getvalue()
  81. assert result is not None
  82. expected = (
  83. f'[File: {temp_file_path} (5 lines total)]\n'
  84. '(this is the beginning of the file)\n'
  85. '1|Line 1\n'
  86. '2|Line 2\n'
  87. '3|Line 3\n'
  88. '4|Line 4\n'
  89. '5|Line 5\n'
  90. '(this is the end of the file)\n'
  91. )
  92. assert result.split('\n') == expected.split('\n')
  93. def test_open_file_with_indentation(tmp_path):
  94. temp_file_path = tmp_path / 'a.txt'
  95. temp_file_path.write_text('Line 1\n Line 2\nLine 3\nLine 4\nLine 5')
  96. with io.StringIO() as buf:
  97. with contextlib.redirect_stdout(buf):
  98. open_file(str(temp_file_path))
  99. result = buf.getvalue()
  100. assert result is not None
  101. expected = (
  102. f'[File: {temp_file_path} (5 lines total)]\n'
  103. '(this is the beginning of the file)\n'
  104. '1|Line 1\n'
  105. '2| Line 2\n'
  106. '3|Line 3\n'
  107. '4|Line 4\n'
  108. '5|Line 5\n'
  109. '(this is the end of the file)\n'
  110. )
  111. assert result.split('\n') == expected.split('\n')
  112. def test_open_file_long(tmp_path):
  113. temp_file_path = tmp_path / 'a.txt'
  114. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  115. temp_file_path.write_text(content)
  116. with io.StringIO() as buf:
  117. with contextlib.redirect_stdout(buf):
  118. open_file(str(temp_file_path), 1, 50)
  119. result = buf.getvalue()
  120. assert result is not None
  121. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  122. expected += '(this is the beginning of the file)\n'
  123. for i in range(1, 51):
  124. expected += f'{i}|Line {i}\n'
  125. expected += '(950 more lines below)\n'
  126. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  127. assert result.split('\n') == expected.split('\n')
  128. def test_open_file_long_with_lineno(tmp_path):
  129. temp_file_path = tmp_path / 'a.txt'
  130. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  131. temp_file_path.write_text(content)
  132. cur_line = 100
  133. with io.StringIO() as buf:
  134. with contextlib.redirect_stdout(buf):
  135. open_file(str(temp_file_path), cur_line)
  136. result = buf.getvalue()
  137. assert result is not None
  138. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  139. # since 100 is < WINDOW and 100 - WINDOW//2 < 0, so it should show all lines from 1 to WINDOW
  140. start, end = _calculate_window_bounds(cur_line, 1000, WINDOW)
  141. if start == 1:
  142. expected += '(this is the beginning of the file)\n'
  143. else:
  144. expected += f'({start - 1} more lines above)\n'
  145. for i in range(start, end + 1):
  146. expected += f'{i}|Line {i}\n'
  147. if end == 1000:
  148. expected += '(this is the end of the file)\n'
  149. else:
  150. expected += f'({1000 - end} more lines below)\n'
  151. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  152. assert result.split('\n') == expected.split('\n')
  153. def test_create_file_unexist_path():
  154. with pytest.raises(FileNotFoundError):
  155. create_file('/unexist/path/a.txt')
  156. def test_create_file(tmp_path):
  157. temp_file_path = tmp_path / 'a.txt'
  158. with io.StringIO() as buf:
  159. with contextlib.redirect_stdout(buf):
  160. create_file(str(temp_file_path))
  161. result = buf.getvalue()
  162. expected = (
  163. f'[File: {temp_file_path} (1 lines total)]\n'
  164. '(this is the beginning of the file)\n'
  165. '1|\n'
  166. '(this is the end of the file)\n'
  167. f'[File {temp_file_path} created.]\n'
  168. )
  169. assert result.split('\n') == expected.split('\n')
  170. def test_goto_line(tmp_path):
  171. temp_file_path = tmp_path / 'a.txt'
  172. total_lines = 1000
  173. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  174. temp_file_path.write_text(content)
  175. with io.StringIO() as buf:
  176. with contextlib.redirect_stdout(buf):
  177. open_file(str(temp_file_path))
  178. result = buf.getvalue()
  179. assert result is not None
  180. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  181. expected += '(this is the beginning of the file)\n'
  182. for i in range(1, WINDOW + 1):
  183. expected += f'{i}|Line {i}\n'
  184. expected += f'({total_lines - WINDOW} more lines below)\n'
  185. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  186. assert result.split('\n') == expected.split('\n')
  187. with io.StringIO() as buf:
  188. with contextlib.redirect_stdout(buf):
  189. goto_line(500)
  190. result = buf.getvalue()
  191. assert result is not None
  192. cur_line = 500
  193. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  194. start, end = _calculate_window_bounds(cur_line, total_lines, WINDOW)
  195. if start == 1:
  196. expected += '(this is the beginning of the file)\n'
  197. else:
  198. expected += f'({start - 1} more lines above)\n'
  199. for i in range(start, end + 1):
  200. expected += f'{i}|Line {i}\n'
  201. if end == total_lines:
  202. expected += '(this is the end of the file)\n'
  203. else:
  204. expected += f'({total_lines - end} more lines below)\n'
  205. assert result.split('\n') == expected.split('\n')
  206. def test_goto_line_negative(tmp_path):
  207. temp_file_path = tmp_path / 'a.txt'
  208. content = '\n'.join([f'Line {i}' for i in range(1, 5)])
  209. temp_file_path.write_text(content)
  210. with io.StringIO() as buf:
  211. with contextlib.redirect_stdout(buf):
  212. open_file(str(temp_file_path))
  213. _capture_file_operation_error(
  214. lambda: goto_line(-1), 'ERROR: Line number must be between 1 and 4.'
  215. )
  216. def test_goto_line_out_of_bound(tmp_path):
  217. temp_file_path = tmp_path / 'a.txt'
  218. content = '\n'.join([f'Line {i}' for i in range(1, 10)])
  219. temp_file_path.write_text(content)
  220. with io.StringIO() as buf:
  221. with contextlib.redirect_stdout(buf):
  222. open_file(str(temp_file_path))
  223. _capture_file_operation_error(
  224. lambda: goto_line(100), 'ERROR: Line number must be between 1 and 9.'
  225. )
  226. def test_scroll_down(tmp_path):
  227. temp_file_path = tmp_path / 'a.txt'
  228. total_lines = 1000
  229. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  230. temp_file_path.write_text(content)
  231. with io.StringIO() as buf:
  232. with contextlib.redirect_stdout(buf):
  233. open_file(str(temp_file_path))
  234. result = buf.getvalue()
  235. assert result is not None
  236. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  237. start, end = _calculate_window_bounds(1, total_lines, WINDOW)
  238. if start == 1:
  239. expected += '(this is the beginning of the file)\n'
  240. else:
  241. expected += f'({start - 1} more lines above)\n'
  242. for i in range(start, end + 1):
  243. expected += f'{i}|Line {i}\n'
  244. if end == total_lines:
  245. expected += '(this is the end of the file)\n'
  246. else:
  247. expected += f'({total_lines - end} more lines below)\n'
  248. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  249. assert result.split('\n') == expected.split('\n')
  250. with io.StringIO() as buf:
  251. with contextlib.redirect_stdout(buf):
  252. scroll_down()
  253. result = buf.getvalue()
  254. assert result is not None
  255. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  256. start = WINDOW + 1
  257. end = 2 * WINDOW + 1
  258. if start == 1:
  259. expected += '(this is the beginning of the file)\n'
  260. else:
  261. expected += f'({start - 1} more lines above)\n'
  262. for i in range(start, end + 1):
  263. expected += f'{i}|Line {i}\n'
  264. if end == total_lines:
  265. expected += '(this is the end of the file)\n'
  266. else:
  267. expected += f'({total_lines - end} more lines below)\n'
  268. assert result.split('\n') == expected.split('\n')
  269. def test_scroll_up(tmp_path):
  270. temp_file_path = tmp_path / 'a.txt'
  271. total_lines = 1000
  272. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  273. temp_file_path.write_text(content)
  274. cur_line = 300
  275. with io.StringIO() as buf:
  276. with contextlib.redirect_stdout(buf):
  277. open_file(str(temp_file_path), cur_line)
  278. result = buf.getvalue()
  279. assert result is not None
  280. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  281. start, end = _calculate_window_bounds(cur_line, total_lines, WINDOW)
  282. if start == 1:
  283. expected += '(this is the beginning of the file)\n'
  284. else:
  285. expected += f'({start - 1} more lines above)\n'
  286. for i in range(start, end + 1):
  287. expected += f'{i}|Line {i}\n'
  288. if end == total_lines:
  289. expected += '(this is the end of the file)\n'
  290. else:
  291. expected += f'({total_lines - end} more lines below)\n'
  292. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  293. assert result.split('\n') == expected.split('\n')
  294. with io.StringIO() as buf:
  295. with contextlib.redirect_stdout(buf):
  296. scroll_up()
  297. result = buf.getvalue()
  298. assert result is not None
  299. cur_line = cur_line - WINDOW
  300. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  301. start = cur_line
  302. end = cur_line + WINDOW
  303. if start == 1:
  304. expected += '(this is the beginning of the file)\n'
  305. else:
  306. expected += f'({start - 1} more lines above)\n'
  307. for i in range(start, end + 1):
  308. expected += f'{i}|Line {i}\n'
  309. if end == total_lines:
  310. expected += '(this is the end of the file)\n'
  311. else:
  312. expected += f'({total_lines - end} more lines below)\n'
  313. assert result.split('\n') == expected.split('\n')
  314. def test_scroll_down_edge(tmp_path):
  315. temp_file_path = tmp_path / 'a.txt'
  316. content = '\n'.join([f'Line {i}' for i in range(1, 10)])
  317. temp_file_path.write_text(content)
  318. with io.StringIO() as buf:
  319. with contextlib.redirect_stdout(buf):
  320. open_file(str(temp_file_path))
  321. result = buf.getvalue()
  322. assert result is not None
  323. expected = f'[File: {temp_file_path} (9 lines total)]\n'
  324. expected += '(this is the beginning of the file)\n'
  325. for i in range(1, 10):
  326. expected += f'{i}|Line {i}\n'
  327. expected += '(this is the end of the file)\n'
  328. with io.StringIO() as buf:
  329. with contextlib.redirect_stdout(buf):
  330. scroll_down()
  331. result = buf.getvalue()
  332. assert result is not None
  333. # expected should be unchanged
  334. assert result.split('\n') == expected.split('\n')
  335. def test_print_window_internal(tmp_path):
  336. test_file_path = tmp_path / 'a.txt'
  337. create_file(str(test_file_path))
  338. open_file(str(test_file_path))
  339. with open(test_file_path, 'w') as file:
  340. for i in range(1, 101):
  341. file.write(f'Line `{i}`\n')
  342. # Define the parameters for the test
  343. current_line = 50
  344. window = 2
  345. # Test _print_window especially with backticks
  346. with io.StringIO() as buf:
  347. with contextlib.redirect_stdout(buf):
  348. _print_window(str(test_file_path), current_line, window, return_str=False)
  349. result = buf.getvalue()
  350. expected = (
  351. '(48 more lines above)\n'
  352. '49|Line `49`\n'
  353. '50|Line `50`\n'
  354. '51|Line `51`\n'
  355. '(49 more lines below)\n'
  356. )
  357. assert result == expected
  358. def test_open_file_large_line_number(tmp_path):
  359. test_file_path = tmp_path / 'a.txt'
  360. create_file(str(test_file_path))
  361. open_file(str(test_file_path))
  362. with open(test_file_path, 'w') as file:
  363. for i in range(1, 1000):
  364. file.write(f'Line `{i}`\n')
  365. # Define the parameters for the test
  366. current_line = 800
  367. window = 100
  368. # Test _print_window especially with backticks
  369. with io.StringIO() as buf:
  370. with contextlib.redirect_stdout(buf):
  371. # _print_window(str(test_file_path), current_line, window, return_str=False)
  372. open_file(str(test_file_path), current_line, window)
  373. result = buf.getvalue()
  374. expected = f'[File: {test_file_path} (999 lines total)]\n'
  375. expected += '(749 more lines above)\n'
  376. for i in range(750, 850 + 1):
  377. expected += f'{i}|Line `{i}`\n'
  378. expected += '(149 more lines below)\n'
  379. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  380. assert result == expected
  381. def test_edit_file_by_replace_window(tmp_path):
  382. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  383. content = """def any_int(a, b, c):
  384. return isinstance(a, int) and isinstance(b, int) and isinstance(c, int)
  385. def test_any_int():
  386. assert any_int(1, 2, 3) == True
  387. assert any_int(1.5, 2, 3) == False
  388. assert any_int(1, 2.5, 3) == False
  389. assert any_int(1, 2, 3.5) == False
  390. assert any_int(1.0, 2, 3) == False
  391. assert any_int(1, 2.0, 3) == False
  392. assert any_int(1, 2, 3.0) == False
  393. assert any_int(0, 0, 0) == True
  394. assert any_int(-1, -2, -3) == True
  395. assert any_int(1, -2, 3) == True
  396. assert any_int(1.5, -2, 3) == False
  397. assert any_int(1, -2.5, 3) == False
  398. def check(any_int):
  399. # Check some simple cases
  400. assert any_int(2, 3, 1)==True, "This prints if this assert fails 1 (good for debugging!)"
  401. assert any_int(2.5, 2, 3)==False, "This prints if this assert fails 2 (good for debugging!)"
  402. assert any_int(1.5, 5, 3.5)==False, "This prints if this assert fails 3 (good for debugging!)"
  403. assert any_int(2, 6, 2)==False, "This prints if this assert fails 4 (good for debugging!)"
  404. assert any_int(4, 2, 2)==True, "This prints if this assert fails 5 (good for debugging!)"
  405. assert any_int(2.2, 2.2, 2.2)==False, "This prints if this assert fails 6 (good for debugging!)"
  406. assert any_int(-4, 6, 2)==True, "This prints if this assert fails 7 (good for debugging!)"
  407. # Check some edge cases that are easy to work out by hand.
  408. assert any_int(2,1,1)==True, "This prints if this assert fails 8 (also good for debugging!)"
  409. assert any_int(3,4,7)==True, "This prints if this assert fails 9 (also good for debugging!)"
  410. assert any_int(3.0,4,7)==False, "This prints if this assert fails 10 (also good for debugging!)"
  411. check(any_int)"""
  412. temp_file_path = tmp_path / 'error-test.py'
  413. temp_file_path.write_text(content)
  414. open_file(str(temp_file_path))
  415. with io.StringIO() as buf:
  416. with contextlib.redirect_stdout(buf):
  417. edit_file_by_replace(
  418. str(temp_file_path),
  419. to_replace=' assert any_int(1.0, 2, 3) == False',
  420. new_content=' assert any_int(1.0, 2, 3) == False',
  421. )
  422. result = buf.getvalue()
  423. expected = (
  424. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  425. 'ERRORS:\n'
  426. + str(temp_file_path)
  427. + ':9:9: '
  428. + 'E999 IndentationError: unexpected indent\n'
  429. '[This is how your edit would have looked if applied]\n'
  430. + SEP
  431. + '(this is the beginning of the file)\n'
  432. '1|def any_int(a, b, c):\n'
  433. '2| return isinstance(a, int) and isinstance(b, int) and isinstance(c, int)\n'
  434. '3|\n'
  435. '4|def test_any_int():\n'
  436. '5| assert any_int(1, 2, 3) == True\n'
  437. '6| assert any_int(1.5, 2, 3) == False\n'
  438. '7| assert any_int(1, 2.5, 3) == False\n'
  439. '8| assert any_int(1, 2, 3.5) == False\n'
  440. '9| assert any_int(1.0, 2, 3) == False\n'
  441. '10| assert any_int(1, 2.0, 3) == False\n'
  442. '11| assert any_int(1, 2, 3.0) == False\n'
  443. '12| assert any_int(0, 0, 0) == True\n'
  444. '13| assert any_int(-1, -2, -3) == True\n'
  445. '14| assert any_int(1, -2, 3) == True\n'
  446. '15| assert any_int(1.5, -2, 3) == False\n'
  447. '16| assert any_int(1, -2.5, 3) == False\n'
  448. '17|\n'
  449. '18|def check(any_int):\n'
  450. '19| # Check some simple cases\n'
  451. '20| assert any_int(2, 3, 1)==True, "This prints if this assert fails 1 (good for debugging!)"\n'
  452. '21| assert any_int(2.5, 2, 3)==False, "This prints if this assert fails 2 (good for debugging!)"\n'
  453. '(12 more lines below)\n' + SEP + '\n'
  454. '[This is the original code before your edit]\n'
  455. + SEP
  456. + '(this is the beginning of the file)\n'
  457. '1|def any_int(a, b, c):\n'
  458. '2| return isinstance(a, int) and isinstance(b, int) and isinstance(c, int)\n'
  459. '3|\n'
  460. '4|def test_any_int():\n'
  461. '5| assert any_int(1, 2, 3) == True\n'
  462. '6| assert any_int(1.5, 2, 3) == False\n'
  463. '7| assert any_int(1, 2.5, 3) == False\n'
  464. '8| assert any_int(1, 2, 3.5) == False\n'
  465. '9| assert any_int(1.0, 2, 3) == False\n'
  466. '10| assert any_int(1, 2.0, 3) == False\n'
  467. '11| assert any_int(1, 2, 3.0) == False\n'
  468. '12| assert any_int(0, 0, 0) == True\n'
  469. '13| assert any_int(-1, -2, -3) == True\n'
  470. '14| assert any_int(1, -2, 3) == True\n'
  471. '15| assert any_int(1.5, -2, 3) == False\n'
  472. '16| assert any_int(1, -2.5, 3) == False\n'
  473. '17|\n'
  474. '18|def check(any_int):\n'
  475. '19| # Check some simple cases\n'
  476. '20| assert any_int(2, 3, 1)==True, "This prints if this assert fails 1 (good for debugging!)"\n'
  477. '21| assert any_int(2.5, 2, 3)==False, "This prints if this assert fails 2 (good for debugging!)"\n'
  478. '(12 more lines below)\n'
  479. + SEP
  480. + 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  481. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  482. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  483. )
  484. assert result == expected
  485. def test_edit_file_by_replace_with_multiple_errors(tmp_path):
  486. # If the file has multiple errors, but the suggested modification can only fix one error, make sure it is applied.
  487. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  488. content = """def Sum(a,b):
  489. try:
  490. answer = a + b
  491. return answer
  492. except Exception:
  493. answer = ANOTHER_CONSTANT
  494. return answer
  495. Sum(1,1)
  496. """
  497. temp_file_path = tmp_path / 'problematic-file-test.py'
  498. temp_file_path.write_text(content)
  499. open_file(str(temp_file_path))
  500. with io.StringIO() as buf:
  501. with contextlib.redirect_stdout(buf):
  502. edit_file_by_replace(
  503. str(temp_file_path),
  504. to_replace=' answer = a + b',
  505. new_content=' answer = a+b',
  506. )
  507. result = buf.getvalue()
  508. expected = (
  509. f'[File: {temp_file_path} (8 lines total after edit)]\n'
  510. '(this is the beginning of the file)\n'
  511. '1|def Sum(a,b):\n'
  512. '2| try:\n'
  513. '3| answer = a+b\n'
  514. '4| return answer\n'
  515. '5| except Exception:\n'
  516. '6| answer = ANOTHER_CONSTANT\n'
  517. '7| return answer\n'
  518. '8|Sum(1,1)\n'
  519. '(this is the end of the file)\n'
  520. + MSG_FILE_UPDATED.format(line_number=3)
  521. + '\n'
  522. )
  523. assert result.split('\n') == expected.split('\n')
  524. # ================================
  525. def test_edit_file_by_replace(tmp_path):
  526. temp_file_path = tmp_path / 'a.txt'
  527. content = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'
  528. temp_file_path.write_text(content)
  529. open_file(str(temp_file_path))
  530. with io.StringIO() as buf:
  531. with contextlib.redirect_stdout(buf):
  532. edit_file_by_replace(
  533. file_name=str(temp_file_path),
  534. to_replace='Line 1\nLine 2\nLine 3',
  535. new_content='REPLACE TEXT',
  536. )
  537. result = buf.getvalue()
  538. expected = (
  539. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  540. '(this is the beginning of the file)\n'
  541. '1|REPLACE TEXT\n'
  542. '2|Line 4\n'
  543. '3|Line 5\n'
  544. '(this is the end of the file)\n'
  545. + MSG_FILE_UPDATED.format(line_number=1)
  546. + '\n'
  547. )
  548. assert result.split('\n') == expected.split('\n')
  549. with open(temp_file_path, 'r') as file:
  550. lines = file.readlines()
  551. assert len(lines) == 3
  552. assert lines[0].rstrip() == 'REPLACE TEXT'
  553. assert lines[1].rstrip() == 'Line 4'
  554. assert lines[2].rstrip() == 'Line 5'
  555. def test_edit_file_by_replace_sameline(tmp_path):
  556. temp_file_path = tmp_path / 'a.txt'
  557. content = 'Line 1\nLine 2\nLine 2\nLine 4\nLine 5'
  558. temp_file_path.write_text(content)
  559. open_file(str(temp_file_path))
  560. with io.StringIO() as buf:
  561. with contextlib.redirect_stdout(buf):
  562. edit_file_by_replace(
  563. file_name=str(temp_file_path),
  564. to_replace='Line 2\nLine 2',
  565. new_content='Line 2\nREPLACE TEXT',
  566. )
  567. result = buf.getvalue()
  568. expected = (
  569. f'[File: {temp_file_path} (5 lines total after edit)]\n'
  570. '(this is the beginning of the file)\n'
  571. '1|Line 1\n'
  572. '2|Line 2\n'
  573. '3|REPLACE TEXT\n'
  574. '4|Line 4\n'
  575. '5|Line 5\n'
  576. '(this is the end of the file)\n'
  577. + MSG_FILE_UPDATED.format(line_number=2)
  578. + '\n'
  579. )
  580. assert result.split('\n') == expected.split('\n')
  581. with open(temp_file_path, 'r') as file:
  582. lines = file.readlines()
  583. assert len(lines) == 5
  584. assert lines[0].rstrip() == 'Line 1'
  585. assert lines[1].rstrip() == 'Line 2'
  586. assert lines[2].rstrip() == 'REPLACE TEXT'
  587. assert lines[3].rstrip() == 'Line 4'
  588. assert lines[4].rstrip() == 'Line 5'
  589. def test_edit_file_by_replace_multiline(tmp_path):
  590. temp_file_path = tmp_path / 'a.txt'
  591. content = 'Line 1\nLine 2\nLine 2\nLine 4\nLine 5'
  592. temp_file_path.write_text(content)
  593. open_file(str(temp_file_path))
  594. with io.StringIO() as buf:
  595. with contextlib.redirect_stdout(buf):
  596. edit_file_by_replace(
  597. file_name=str(temp_file_path),
  598. to_replace='Line 2',
  599. new_content='REPLACE TEXT',
  600. )
  601. result = buf.getvalue()
  602. assert result.strip().startswith(
  603. 'ERROR: `to_replace` appears more than once, please include enough lines to make code in `to_replace` unique'
  604. )
  605. def test_edit_file_by_replace_no_diff(tmp_path):
  606. temp_file_path = tmp_path / 'a.txt'
  607. content = 'Line 1\nLine 2\nLine 2\nLine 4\nLine 5'
  608. temp_file_path.write_text(content)
  609. open_file(str(temp_file_path))
  610. with io.StringIO() as buf:
  611. with contextlib.redirect_stdout(buf):
  612. edit_file_by_replace(
  613. file_name=str(temp_file_path),
  614. to_replace='Line 1',
  615. new_content='Line 1',
  616. )
  617. result = buf.getvalue()
  618. assert result.strip().startswith(
  619. 'ERROR: `to_replace` and `new_content` must be different'
  620. )
  621. def test_edit_file_by_replace_toreplace_empty(tmp_path):
  622. temp_file_path = tmp_path / 'a.txt'
  623. content = 'Line 1\nLine 2\nLine 2\nLine 4\nLine 5'
  624. temp_file_path.write_text(content)
  625. open_file(str(temp_file_path))
  626. _capture_file_operation_error(
  627. lambda: edit_file_by_replace(
  628. file_name=str(temp_file_path),
  629. to_replace='',
  630. new_content='Line 1',
  631. ),
  632. 'ERROR: `to_replace` must not be empty.',
  633. )
  634. def test_edit_file_by_replace_unknown_file():
  635. _capture_file_operation_error(
  636. lambda: edit_file_by_replace(
  637. str('unknown file'),
  638. 'ORIGINAL TEXT',
  639. 'REPLACE TEXT',
  640. ),
  641. 'ERROR: File unknown file not found.',
  642. )
  643. def test_insert_content_at_line(tmp_path):
  644. temp_file_path = tmp_path / 'b.txt'
  645. content = 'Line 1\nLine 2\nLine 3'
  646. temp_file_path.write_text(content)
  647. open_file(str(temp_file_path))
  648. with io.StringIO() as buf:
  649. with contextlib.redirect_stdout(buf):
  650. insert_content_at_line(
  651. file_name=str(temp_file_path),
  652. line_number=2,
  653. content='Inserted Line',
  654. )
  655. result = buf.getvalue()
  656. expected = (
  657. f'[File: {temp_file_path} (4 lines total after edit)]\n'
  658. '(this is the beginning of the file)\n'
  659. '1|Line 1\n'
  660. '2|Inserted Line\n'
  661. '3|Line 2\n'
  662. '4|Line 3\n'
  663. '(this is the end of the file)\n'
  664. + MSG_FILE_UPDATED.format(line_number=2)
  665. + '\n'
  666. )
  667. assert result.split('\n') == expected.split('\n')
  668. with open(temp_file_path, 'r') as file:
  669. lines = file.readlines()
  670. assert len(lines) == 4
  671. assert lines[0].rstrip() == 'Line 1'
  672. assert lines[1].rstrip() == 'Inserted Line'
  673. assert lines[2].rstrip() == 'Line 2'
  674. assert lines[3].rstrip() == 'Line 3'
  675. def test_insert_content_at_line_from_scratch(tmp_path):
  676. temp_file_path = tmp_path / 'a.txt'
  677. create_file(str(temp_file_path))
  678. open_file(str(temp_file_path))
  679. with io.StringIO() as buf:
  680. with contextlib.redirect_stdout(buf):
  681. insert_content_at_line(
  682. file_name=str(temp_file_path),
  683. line_number=1,
  684. content='REPLACE TEXT',
  685. )
  686. result = buf.getvalue()
  687. expected = (
  688. f'[File: {temp_file_path} (1 lines total after edit)]\n'
  689. '(this is the beginning of the file)\n'
  690. '1|REPLACE TEXT\n'
  691. '(this is the end of the file)\n'
  692. + MSG_FILE_UPDATED.format(line_number=1)
  693. + '\n'
  694. )
  695. assert result.split('\n') == expected.split('\n')
  696. with open(temp_file_path, 'r') as file:
  697. lines = file.readlines()
  698. assert len(lines) == 1
  699. assert lines[0].rstrip() == 'REPLACE TEXT'
  700. def test_insert_content_at_line_from_scratch_emptyfile(tmp_path):
  701. temp_file_path = tmp_path / 'a.txt'
  702. with open(temp_file_path, 'w') as file:
  703. file.write('')
  704. open_file(str(temp_file_path))
  705. with io.StringIO() as buf:
  706. with contextlib.redirect_stdout(buf):
  707. insert_content_at_line(
  708. file_name=str(temp_file_path),
  709. line_number=1,
  710. content='REPLACE TEXT',
  711. )
  712. result = buf.getvalue()
  713. expected = (
  714. f'[File: {temp_file_path} (1 lines total after edit)]\n'
  715. '(this is the beginning of the file)\n'
  716. '1|REPLACE TEXT\n'
  717. '(this is the end of the file)\n'
  718. + MSG_FILE_UPDATED.format(line_number=1)
  719. + '\n'
  720. )
  721. assert result.split('\n') == expected.split('\n')
  722. with open(temp_file_path, 'r') as file:
  723. lines = file.readlines()
  724. assert len(lines) == 1
  725. assert lines[0].rstrip() == 'REPLACE TEXT'
  726. def test_insert_content_at_line_emptyline(tmp_path):
  727. temp_file_path = tmp_path / 'b.txt'
  728. content = 'Line 1\n\n'
  729. temp_file_path.write_text(content)
  730. open_file(str(temp_file_path))
  731. with io.StringIO() as buf:
  732. with contextlib.redirect_stdout(buf):
  733. insert_content_at_line(
  734. file_name=str(temp_file_path),
  735. line_number=2,
  736. content='Inserted Line',
  737. )
  738. result = buf.getvalue()
  739. expected = (
  740. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  741. '(this is the beginning of the file)\n'
  742. '1|Line 1\n'
  743. '2|Inserted Line\n'
  744. '3|\n'
  745. '(this is the end of the file)\n'
  746. + MSG_FILE_UPDATED.format(line_number=2)
  747. + '\n'
  748. )
  749. assert result.split('\n') == expected.split('\n')
  750. with open(temp_file_path, 'r') as file:
  751. lines = file.readlines()
  752. assert len(lines) == 3
  753. assert lines[0].rstrip() == 'Line 1'
  754. assert lines[1].rstrip() == 'Inserted Line'
  755. def test_insert_content_at_line_from_scratch_multiline_with_backticks_and_second_edit(
  756. tmp_path,
  757. ):
  758. temp_file_path = tmp_path / 'a.txt'
  759. create_file(str(temp_file_path))
  760. open_file(str(temp_file_path))
  761. with io.StringIO() as buf:
  762. with contextlib.redirect_stdout(buf):
  763. insert_content_at_line(
  764. str(temp_file_path),
  765. 1,
  766. '`REPLACE TEXT1`\n`REPLACE TEXT2`\n`REPLACE TEXT3`',
  767. )
  768. result = buf.getvalue()
  769. expected = (
  770. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  771. '(this is the beginning of the file)\n'
  772. '1|`REPLACE TEXT1`\n'
  773. '2|`REPLACE TEXT2`\n'
  774. '3|`REPLACE TEXT3`\n'
  775. '(this is the end of the file)\n'
  776. + MSG_FILE_UPDATED.format(line_number=1)
  777. + '\n'
  778. )
  779. assert result.split('\n') == expected.split('\n')
  780. with open(temp_file_path, 'r') as file:
  781. lines = file.readlines()
  782. assert len(lines) == 3
  783. assert lines[0].rstrip() == '`REPLACE TEXT1`'
  784. assert lines[1].rstrip() == '`REPLACE TEXT2`'
  785. assert lines[2].rstrip() == '`REPLACE TEXT3`'
  786. # Check that no backticks are escaped in the edit_file_by_replace call
  787. assert '\\`' not in result
  788. # Perform a second edit
  789. with io.StringIO() as buf:
  790. with contextlib.redirect_stdout(buf):
  791. edit_file_by_replace(
  792. str(temp_file_path),
  793. '`REPLACE TEXT1`\n`REPLACE TEXT2`\n`REPLACE TEXT3`',
  794. '`REPLACED TEXT1`\n`REPLACED TEXT2`\n`REPLACED TEXT3`',
  795. )
  796. second_result = buf.getvalue()
  797. second_expected = (
  798. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  799. '(this is the beginning of the file)\n'
  800. '1|`REPLACED TEXT1`\n'
  801. '2|`REPLACED TEXT2`\n'
  802. '3|`REPLACED TEXT3`\n'
  803. '(this is the end of the file)\n'
  804. + MSG_FILE_UPDATED.format(line_number=1)
  805. + '\n'
  806. )
  807. assert second_result.split('\n') == second_expected.split('\n')
  808. with open(temp_file_path, 'r') as file:
  809. lines = file.readlines()
  810. assert len(lines) == 3
  811. assert lines[0].rstrip() == '`REPLACED TEXT1`'
  812. assert lines[1].rstrip() == '`REPLACED TEXT2`'
  813. assert lines[2].rstrip() == '`REPLACED TEXT3`'
  814. # Check that no backticks are escaped in the second edit_file_by_replace call
  815. assert '\\`' not in second_result
  816. def test_insert_content_at_line_from_scratch_multiline(tmp_path):
  817. temp_file_path = tmp_path / 'a.txt'
  818. create_file(str(temp_file_path))
  819. open_file(temp_file_path)
  820. with io.StringIO() as buf:
  821. with contextlib.redirect_stdout(buf):
  822. insert_content_at_line(
  823. str(temp_file_path),
  824. 1,
  825. content='REPLACE TEXT1\nREPLACE TEXT2\nREPLACE TEXT3',
  826. )
  827. result = buf.getvalue()
  828. expected = (
  829. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  830. '(this is the beginning of the file)\n'
  831. '1|REPLACE TEXT1\n'
  832. '2|REPLACE TEXT2\n'
  833. '3|REPLACE TEXT3\n'
  834. '(this is the end of the file)\n'
  835. + MSG_FILE_UPDATED.format(line_number=1)
  836. + '\n'
  837. )
  838. assert result.split('\n') == expected.split('\n')
  839. with open(temp_file_path, 'r') as file:
  840. lines = file.readlines()
  841. assert len(lines) == 3
  842. assert lines[0].rstrip() == 'REPLACE TEXT1'
  843. assert lines[1].rstrip() == 'REPLACE TEXT2'
  844. assert lines[2].rstrip() == 'REPLACE TEXT3'
  845. def test_insert_content_at_line_not_opened():
  846. _capture_file_operation_error(
  847. lambda: insert_content_at_line(
  848. str('unknown file'),
  849. 1,
  850. 'REPLACE TEXT',
  851. ),
  852. 'ERROR: Invalid path or file name.',
  853. )
  854. def test_append_file(tmp_path):
  855. temp_file_path = tmp_path / 'a.txt'
  856. content = 'Line 1\nLine 2'
  857. temp_file_path.write_text(content)
  858. open_file(str(temp_file_path))
  859. with io.StringIO() as buf:
  860. with contextlib.redirect_stdout(buf):
  861. append_file(str(temp_file_path), content='APPENDED TEXT')
  862. result = buf.getvalue()
  863. expected = (
  864. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  865. '(this is the beginning of the file)\n'
  866. '1|Line 1\n'
  867. '2|Line 2\n'
  868. '3|APPENDED TEXT\n'
  869. '(this is the end of the file)\n'
  870. + MSG_FILE_UPDATED.format(line_number=2)
  871. + '\n'
  872. )
  873. assert result.split('\n') == expected.split('\n')
  874. with open(temp_file_path, 'r') as file:
  875. lines = file.readlines()
  876. assert len(lines) == 3
  877. assert lines[0].rstrip() == 'Line 1'
  878. assert lines[1].rstrip() == 'Line 2'
  879. assert lines[2].rstrip() == 'APPENDED TEXT'
  880. def test_append_file_from_scratch(tmp_path):
  881. temp_file_path = tmp_path / 'a.txt'
  882. create_file(str(temp_file_path))
  883. try:
  884. open_file(str(temp_file_path))
  885. with io.StringIO() as buf:
  886. with contextlib.redirect_stdout(buf):
  887. append_file(str(temp_file_path), content='APPENDED TEXT')
  888. result = buf.getvalue()
  889. expected = (
  890. f'[File: {temp_file_path} (1 lines total after edit)]\n'
  891. '(this is the beginning of the file)\n'
  892. '1|APPENDED TEXT\n'
  893. '(this is the end of the file)\n'
  894. + MSG_FILE_UPDATED.format(line_number=1)
  895. + '\n'
  896. )
  897. assert result.split('\n') == expected.split('\n')
  898. with open(temp_file_path, 'r') as file:
  899. lines = file.readlines()
  900. assert len(lines) == 1
  901. assert lines[0].rstrip() == 'APPENDED TEXT'
  902. finally:
  903. os.remove(temp_file_path)
  904. def test_append_file_from_scratch_multiline(tmp_path):
  905. temp_file_path = tmp_path / 'a3.txt'
  906. create_file(str(temp_file_path))
  907. try:
  908. open_file(temp_file_path)
  909. with io.StringIO() as buf:
  910. with contextlib.redirect_stdout(buf):
  911. append_file(
  912. str(temp_file_path),
  913. content='APPENDED TEXT1\nAPPENDED TEXT2\nAPPENDED TEXT3',
  914. )
  915. result = buf.getvalue()
  916. expected = (
  917. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  918. '(this is the beginning of the file)\n'
  919. '1|APPENDED TEXT1\n'
  920. '2|APPENDED TEXT2\n'
  921. '3|APPENDED TEXT3\n'
  922. '(this is the end of the file)\n'
  923. + MSG_FILE_UPDATED.format(line_number=1)
  924. + '\n'
  925. )
  926. assert result.split('\n') == expected.split('\n')
  927. with open(temp_file_path, 'r') as file:
  928. lines = file.readlines()
  929. assert len(lines) == 3
  930. assert lines[0].rstrip() == 'APPENDED TEXT1'
  931. assert lines[1].rstrip() == 'APPENDED TEXT2'
  932. assert lines[2].rstrip() == 'APPENDED TEXT3'
  933. finally:
  934. os.remove(temp_file_path)
  935. def test_append_file_not_opened():
  936. _capture_file_operation_error(
  937. lambda: append_file('unknown file', content='APPENDED TEXT'),
  938. 'ERROR: Invalid path or file name.',
  939. )
  940. def test_search_dir(tmp_path):
  941. # create files with the search term "bingo"
  942. for i in range(1, 101):
  943. temp_file_path = tmp_path / f'a{i}.txt'
  944. with open(temp_file_path, 'w') as file:
  945. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  946. if i == 50:
  947. file.write('bingo')
  948. # test
  949. with io.StringIO() as buf:
  950. with contextlib.redirect_stdout(buf):
  951. search_dir('bingo', str(tmp_path))
  952. result = buf.getvalue()
  953. assert result is not None
  954. expected = (
  955. f'[Found 1 matches for "bingo" in {tmp_path}]\n'
  956. f'{tmp_path}/a50.txt (Line 6): bingo\n'
  957. f'[End of matches for "bingo" in {tmp_path}]\n'
  958. )
  959. assert result.split('\n') == expected.split('\n')
  960. def test_search_dir_not_exist_term(tmp_path):
  961. # create files with the search term "bingo"
  962. for i in range(1, 101):
  963. temp_file_path = tmp_path / f'a{i}.txt'
  964. with open(temp_file_path, 'w') as file:
  965. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  966. # test
  967. with io.StringIO() as buf:
  968. with contextlib.redirect_stdout(buf):
  969. search_dir('non-exist', str(tmp_path))
  970. result = buf.getvalue()
  971. assert result is not None
  972. expected = f'No matches found for "non-exist" in {tmp_path}\n'
  973. assert result.split('\n') == expected.split('\n')
  974. def test_search_dir_too_much_match(tmp_path):
  975. # create files with the search term "Line 5"
  976. for i in range(1, 1000):
  977. temp_file_path = tmp_path / f'a{i}.txt'
  978. with open(temp_file_path, 'w') as file:
  979. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  980. with io.StringIO() as buf:
  981. with contextlib.redirect_stdout(buf):
  982. search_dir('Line 5', str(tmp_path))
  983. result = buf.getvalue()
  984. assert result is not None
  985. expected = f'More than 999 files matched for "Line 5" in {tmp_path}. Please narrow your search.\n'
  986. assert result.split('\n') == expected.split('\n')
  987. def test_search_dir_cwd(tmp_path, monkeypatch):
  988. # Using pytest's monkeypatch to change directory without affecting other tests
  989. monkeypatch.chdir(tmp_path)
  990. # create files with the search term "bingo"
  991. for i in range(1, 101):
  992. temp_file_path = tmp_path / f'a{i}.txt'
  993. with open(temp_file_path, 'w') as file:
  994. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  995. if i == 50:
  996. file.write('bingo')
  997. with io.StringIO() as buf:
  998. with contextlib.redirect_stdout(buf):
  999. search_dir('bingo')
  1000. result = buf.getvalue()
  1001. assert result is not None
  1002. expected = (
  1003. '[Found 1 matches for "bingo" in ./]\n'
  1004. './a50.txt (Line 6): bingo\n'
  1005. '[End of matches for "bingo" in ./]\n'
  1006. )
  1007. assert result.split('\n') == expected.split('\n')
  1008. def test_search_file(tmp_path):
  1009. temp_file_path = tmp_path / 'a.txt'
  1010. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  1011. with io.StringIO() as buf:
  1012. with contextlib.redirect_stdout(buf):
  1013. search_file('Line 5', str(temp_file_path))
  1014. result = buf.getvalue()
  1015. assert result is not None
  1016. expected = f'[Found 1 matches for "Line 5" in {temp_file_path}]\n'
  1017. expected += 'Line 5: Line 5\n'
  1018. expected += f'[End of matches for "Line 5" in {temp_file_path}]\n'
  1019. assert result.split('\n') == expected.split('\n')
  1020. def test_search_file_not_exist_term(tmp_path):
  1021. temp_file_path = tmp_path / 'a.txt'
  1022. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  1023. with io.StringIO() as buf:
  1024. with contextlib.redirect_stdout(buf):
  1025. search_file('Line 6', str(temp_file_path))
  1026. result = buf.getvalue()
  1027. assert result is not None
  1028. expected = f'[No matches found for "Line 6" in {temp_file_path}]\n'
  1029. assert result.split('\n') == expected.split('\n')
  1030. def test_search_file_not_exist_file():
  1031. _capture_file_operation_error(
  1032. lambda: search_file('Line 6', '/unexist/path/a.txt'),
  1033. 'ERROR: File /unexist/path/a.txt not found.',
  1034. )
  1035. def test_find_file(tmp_path):
  1036. temp_file_path = tmp_path / 'a.txt'
  1037. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  1038. with io.StringIO() as buf:
  1039. with contextlib.redirect_stdout(buf):
  1040. find_file('a.txt', str(tmp_path))
  1041. result = buf.getvalue()
  1042. assert result is not None
  1043. expected = f'[Found 1 matches for "a.txt" in {tmp_path}]\n'
  1044. expected += f'{tmp_path}/a.txt\n'
  1045. expected += f'[End of matches for "a.txt" in {tmp_path}]\n'
  1046. assert result.split('\n') == expected.split('\n')
  1047. def test_find_file_cwd(tmp_path, monkeypatch):
  1048. monkeypatch.chdir(tmp_path)
  1049. temp_file_path = tmp_path / 'a.txt'
  1050. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  1051. with io.StringIO() as buf:
  1052. with contextlib.redirect_stdout(buf):
  1053. find_file('a.txt')
  1054. result = buf.getvalue()
  1055. assert result is not None
  1056. def test_find_file_not_exist_file():
  1057. with io.StringIO() as buf:
  1058. with contextlib.redirect_stdout(buf):
  1059. find_file('nonexist.txt')
  1060. result = buf.getvalue()
  1061. assert result is not None
  1062. expected = '[No matches found for "nonexist.txt" in ./]\n'
  1063. assert result.split('\n') == expected.split('\n')
  1064. def test_find_file_not_exist_file_specific_path(tmp_path):
  1065. with io.StringIO() as buf:
  1066. with contextlib.redirect_stdout(buf):
  1067. find_file('nonexist.txt', str(tmp_path))
  1068. result = buf.getvalue()
  1069. assert result is not None
  1070. expected = f'[No matches found for "nonexist.txt" in {tmp_path}]\n'
  1071. assert result.split('\n') == expected.split('\n')
  1072. def test_edit_lint_file_pass(tmp_path):
  1073. # Enable linting
  1074. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  1075. file_path = _generate_test_file_with_lines(tmp_path, 1)
  1076. # Test linting functionality
  1077. with io.StringIO() as buf:
  1078. with contextlib.redirect_stdout(buf):
  1079. open_file(str(file_path))
  1080. insert_content_at_line(str(file_path), 1, "print('hello')\n")
  1081. result = buf.getvalue()
  1082. assert result is not None
  1083. expected = (
  1084. f'[File: {file_path} (1 lines total)]\n'
  1085. '(this is the beginning of the file)\n'
  1086. '1|\n'
  1087. '(this is the end of the file)\n'
  1088. f'[File: {file_path} (1 lines total after edit)]\n'
  1089. '(this is the beginning of the file)\n'
  1090. "1|print('hello')\n"
  1091. '(this is the end of the file)\n'
  1092. + MSG_FILE_UPDATED.format(line_number=1)
  1093. + '\n'
  1094. )
  1095. assert result.split('\n') == expected.split('\n')
  1096. def test_lint_file_fail_undefined_name(tmp_path, capsys):
  1097. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  1098. current_line = 1
  1099. file_path = _generate_test_file_with_lines(tmp_path, 1)
  1100. open_file(str(file_path), current_line)
  1101. insert_content_at_line(str(file_path), 1, 'undefined_name()\n')
  1102. result = capsys.readouterr().out
  1103. assert result is not None
  1104. expected = (
  1105. f'[File: {file_path} (1 lines total)]\n'
  1106. '(this is the beginning of the file)\n'
  1107. '1|\n'
  1108. '(this is the end of the file)\n'
  1109. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  1110. 'ERRORS:\n'
  1111. f"{file_path}:1:1: F821 undefined name 'undefined_name'\n"
  1112. '[This is how your edit would have looked if applied]\n'
  1113. + SEP
  1114. + '(this is the beginning of the file)\n'
  1115. '1|undefined_name()\n'
  1116. '(this is the end of the file)\n'
  1117. + SEP
  1118. + '\n[This is the original code before your edit]\n'
  1119. + SEP
  1120. + '(this is the beginning of the file)\n'
  1121. '1|\n'
  1122. '(this is the end of the file)\n'
  1123. + SEP
  1124. + 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  1125. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  1126. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  1127. )
  1128. assert result.split('\n') == expected.split('\n')
  1129. def test_lint_file_fail_undefined_name_long(tmp_path, capsys):
  1130. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  1131. num_lines = 1000
  1132. error_line = 500
  1133. file_path = _generate_test_file_with_lines(tmp_path, num_lines)
  1134. error_message = (
  1135. f"{file_path}:{error_line}:1: F821 undefined name 'undefined_name'"
  1136. )
  1137. open_file(str(file_path))
  1138. insert_content_at_line(str(file_path), error_line, 'undefined_name()\n')
  1139. result = capsys.readouterr().out
  1140. assert result is not None
  1141. open_lines = '\n'.join([f'{i}|' for i in range(1, WINDOW + 1)])
  1142. expected = (
  1143. f'[File: {file_path} ({num_lines} lines total)]\n'
  1144. '(this is the beginning of the file)\n'
  1145. f'{open_lines}\n'
  1146. f'({num_lines - WINDOW} more lines below)\n'
  1147. f'[Use `scroll_down` to view the next 100 lines of the file!]\n'
  1148. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  1149. f'ERRORS:\n{error_message}\n'
  1150. '[This is how your edit would have looked if applied]\n'
  1151. + SEP
  1152. + '(489 more lines above)\n'
  1153. + _numbered_test_lines(error_line - 10, error_line - 1)
  1154. + '500|undefined_name()\n'
  1155. + _numbered_test_lines(error_line + 1, error_line + 10)
  1156. + '(491 more lines below)\n'
  1157. + SEP
  1158. + '\n[This is the original code before your edit]\n'
  1159. + SEP
  1160. + '(489 more lines above)\n'
  1161. + _numbered_test_lines(error_line - 10, error_line + 10)
  1162. + '(490 more lines below)\n'
  1163. + SEP
  1164. + 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  1165. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  1166. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  1167. )
  1168. assert result.split('\n') == expected.split('\n')
  1169. def test_lint_file_disabled_undefined_name(tmp_path, capsys):
  1170. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'False'}):
  1171. file_path = _generate_test_file_with_lines(tmp_path, 1)
  1172. open_file(str(file_path))
  1173. insert_content_at_line(str(file_path), 1, 'undefined_name()\n')
  1174. result = capsys.readouterr().out
  1175. assert result is not None
  1176. expected = (
  1177. f'[File: {file_path} (1 lines total)]\n'
  1178. '(this is the beginning of the file)\n'
  1179. '1|\n'
  1180. '(this is the end of the file)\n'
  1181. f'[File: {file_path} (1 lines total after edit)]\n'
  1182. '(this is the beginning of the file)\n'
  1183. '1|undefined_name()\n'
  1184. '(this is the end of the file)\n'
  1185. + MSG_FILE_UPDATED.format(line_number=1)
  1186. + '\n'
  1187. )
  1188. assert result.split('\n') == expected.split('\n')
  1189. def test_parse_docx(tmp_path):
  1190. # Create a DOCX file with some content
  1191. test_docx_path = tmp_path / 'test.docx'
  1192. doc = docx.Document()
  1193. doc.add_paragraph('Hello, this is a test document.')
  1194. doc.add_paragraph('This is the second paragraph.')
  1195. doc.save(str(test_docx_path))
  1196. old_stdout = sys.stdout
  1197. sys.stdout = io.StringIO()
  1198. # Call the parse_docx function
  1199. parse_docx(str(test_docx_path))
  1200. # Capture the output
  1201. output = sys.stdout.getvalue()
  1202. sys.stdout = old_stdout
  1203. # Check if the output is correct
  1204. expected_output = (
  1205. f'[Reading DOCX file from {test_docx_path}]\n'
  1206. '@@ Page 1 @@\nHello, this is a test document.\n\n'
  1207. '@@ Page 2 @@\nThis is the second paragraph.\n\n\n'
  1208. )
  1209. assert output == expected_output, f'Expected output does not match. Got: {output}'
  1210. def test_parse_latex(tmp_path):
  1211. # Create a LaTeX file with some content
  1212. test_latex_path = tmp_path / 'test.tex'
  1213. with open(test_latex_path, 'w') as f:
  1214. f.write(r"""
  1215. \documentclass{article}
  1216. \begin{document}
  1217. Hello, this is a test LaTeX document.
  1218. \end{document}
  1219. """)
  1220. old_stdout = sys.stdout
  1221. sys.stdout = io.StringIO()
  1222. # Call the parse_latex function
  1223. parse_latex(str(test_latex_path))
  1224. # Capture the output
  1225. output = sys.stdout.getvalue()
  1226. sys.stdout = old_stdout
  1227. # Check if the output is correct
  1228. expected_output = (
  1229. f'[Reading LaTex file from {test_latex_path}]\n'
  1230. 'Hello, this is a test LaTeX document.\n'
  1231. )
  1232. assert output == expected_output, f'Expected output does not match. Got: {output}'
  1233. def test_parse_pdf(tmp_path):
  1234. # Create a PDF file with some content
  1235. test_pdf_path = tmp_path / 'test.pdf'
  1236. from reportlab.lib.pagesizes import letter
  1237. from reportlab.pdfgen import canvas
  1238. c = canvas.Canvas(str(test_pdf_path), pagesize=letter)
  1239. c.drawString(100, 750, 'Hello, this is a test PDF document.')
  1240. c.save()
  1241. old_stdout = sys.stdout
  1242. sys.stdout = io.StringIO()
  1243. # Call the parse_pdf function
  1244. parse_pdf(str(test_pdf_path))
  1245. # Capture the output
  1246. output = sys.stdout.getvalue()
  1247. sys.stdout = old_stdout
  1248. # Check if the output is correct
  1249. expected_output = (
  1250. f'[Reading PDF file from {test_pdf_path}]\n'
  1251. '@@ Page 1 @@\n'
  1252. 'Hello, this is a test PDF document.\n'
  1253. )
  1254. assert output == expected_output, f'Expected output does not match. Got: {output}'
  1255. def test_parse_pptx(tmp_path):
  1256. test_pptx_path = tmp_path / 'test.pptx'
  1257. from pptx import Presentation
  1258. pres = Presentation()
  1259. slide1 = pres.slides.add_slide(pres.slide_layouts[0])
  1260. title1 = slide1.shapes.title
  1261. title1.text = 'Hello, this is the first test PPTX slide.'
  1262. slide2 = pres.slides.add_slide(pres.slide_layouts[0])
  1263. title2 = slide2.shapes.title
  1264. title2.text = 'Hello, this is the second test PPTX slide.'
  1265. pres.save(str(test_pptx_path))
  1266. old_stdout = sys.stdout
  1267. sys.stdout = io.StringIO()
  1268. parse_pptx(str(test_pptx_path))
  1269. output = sys.stdout.getvalue()
  1270. sys.stdout = old_stdout
  1271. expected_output = (
  1272. f'[Reading PowerPoint file from {test_pptx_path}]\n'
  1273. '@@ Slide 1 @@\n'
  1274. 'Hello, this is the first test PPTX slide.\n\n'
  1275. '@@ Slide 2 @@\n'
  1276. 'Hello, this is the second test PPTX slide.\n\n'
  1277. )
  1278. assert output == expected_output, f'Expected output does not match. Got: {output}'
  1279. def test_lint_file_fail_non_python(tmp_path, capsys):
  1280. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  1281. current_line = 1
  1282. file_path = _generate_ruby_test_file_with_lines(tmp_path, 1)
  1283. open_file(str(file_path), current_line)
  1284. insert_content_at_line(
  1285. str(file_path), 1, "def print_hello_world()\n puts 'Hello World'"
  1286. )
  1287. result = capsys.readouterr().out
  1288. assert result is not None
  1289. expected = (
  1290. f'[File: {file_path} (1 lines total)]\n'
  1291. '(this is the beginning of the file)\n'
  1292. '1|\n'
  1293. '(this is the end of the file)\n'
  1294. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  1295. 'ERRORS:\n'
  1296. f'{file_path}:1:1: Syntax error\n'
  1297. '[This is how your edit would have looked if applied]\n'
  1298. + SEP
  1299. + '(this is the beginning of the file)\n'
  1300. '1|def print_hello_world()\n'
  1301. "2| puts 'Hello World'\n"
  1302. '(this is the end of the file)\n'
  1303. '-------------------------------------------------\n\n'
  1304. '[This is the original code before your edit]\n'
  1305. + SEP
  1306. + '(this is the beginning of the file)\n'
  1307. '1|\n'
  1308. '(this is the end of the file)\n'
  1309. + SEP
  1310. + 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  1311. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  1312. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  1313. )
  1314. assert result.split('\n') == expected.split('\n')
  1315. def test_lint_file_fail_typescript(tmp_path, capsys):
  1316. linter = Linter()
  1317. with patch.dict(os.environ, {'ENABLE_AUTO_LINT': 'True'}):
  1318. current_line = 1
  1319. file_path = tmp_path / 'test.ts'
  1320. file_path.write_text('')
  1321. open_file(str(file_path), current_line)
  1322. insert_content_at_line(
  1323. str(file_path),
  1324. 1,
  1325. "function greet(name: string) {\n console.log('Hello, ' + name)",
  1326. )
  1327. result = capsys.readouterr().out
  1328. assert result is not None
  1329. # Note: the tsc (typescript compiler) message is different from a
  1330. # compared to a python linter message, like line and column in brackets:
  1331. expected_lines = [
  1332. f'[File: {file_path} (1 lines total)]',
  1333. '(this is the beginning of the file)',
  1334. '1|',
  1335. '(this is the end of the file)',
  1336. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]',
  1337. 'ERRORS:',
  1338. f"{file_path}(3,1): error TS1005: '}}' expected.",
  1339. '[This is how your edit would have looked if applied]',
  1340. '-------------------------------------------------',
  1341. '(this is the beginning of the file)',
  1342. '1|function greet(name: string) {',
  1343. "2| console.log('Hello, ' + name)",
  1344. '(this is the end of the file)',
  1345. '-------------------------------------------------',
  1346. '',
  1347. '[This is the original code before your edit]',
  1348. '-------------------------------------------------',
  1349. '(this is the beginning of the file)',
  1350. '1|',
  1351. '(this is the end of the file)',
  1352. '-------------------------------------------------',
  1353. 'Your changes have NOT been applied. Please fix your edit command and try again.',
  1354. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.',
  1355. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.',
  1356. '',
  1357. ]
  1358. result_lines = result.split('\n')
  1359. assert len(result_lines) == len(expected_lines), "Number of lines doesn't match"
  1360. for i, (result_line, expected_line) in enumerate(
  1361. zip(result_lines, expected_lines)
  1362. ):
  1363. if i == 6:
  1364. if linter.ts_installed and result_line != expected_lines[6]:
  1365. assert (
  1366. 'ts:1:20:' in result_line or '(3,1):' in result_line
  1367. ), f"Line {i+1} doesn't match"
  1368. else:
  1369. assert result_line.lstrip('./') == expected_line.lstrip(
  1370. './'
  1371. ), f"Line {i+1} doesn't match"