test_agent_skill.py 58 KB

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