test_agent_skill.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. import contextlib
  2. import io
  3. import os
  4. import sys
  5. import docx
  6. import pytest
  7. from opendevin.runtime.plugins.agent_skills.agentskills import (
  8. MSG_FILE_UPDATED,
  9. _print_window,
  10. append_file,
  11. create_file,
  12. edit_file,
  13. find_file,
  14. goto_line,
  15. open_file,
  16. parse_docx,
  17. parse_latex,
  18. parse_pdf,
  19. parse_pptx,
  20. scroll_down,
  21. scroll_up,
  22. search_dir,
  23. search_file,
  24. )
  25. # CURRENT_FILE must be reset for each test
  26. @pytest.fixture(autouse=True)
  27. def reset_current_file():
  28. from opendevin.runtime.plugins.agent_skills import agentskills
  29. agentskills.CURRENT_FILE = None
  30. def test_open_file_unexist_path():
  31. with pytest.raises(FileNotFoundError):
  32. open_file('/unexist/path/a.txt')
  33. def test_open_file(tmp_path):
  34. assert tmp_path is not None
  35. temp_file_path = tmp_path / 'a.txt'
  36. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  37. with io.StringIO() as buf:
  38. with contextlib.redirect_stdout(buf):
  39. open_file(str(temp_file_path))
  40. result = buf.getvalue()
  41. assert result is not None
  42. expected = (
  43. f'[File: {temp_file_path} (5 lines total)]\n'
  44. '1|Line 1\n'
  45. '2|Line 2\n'
  46. '3|Line 3\n'
  47. '4|Line 4\n'
  48. '5|Line 5\n'
  49. )
  50. assert result.split('\n') == expected.split('\n')
  51. def test_open_file_with_indentation(tmp_path):
  52. temp_file_path = tmp_path / 'a.txt'
  53. temp_file_path.write_text('Line 1\n Line 2\nLine 3\nLine 4\nLine 5')
  54. with io.StringIO() as buf:
  55. with contextlib.redirect_stdout(buf):
  56. open_file(str(temp_file_path))
  57. result = buf.getvalue()
  58. assert result is not None
  59. expected = (
  60. f'[File: {temp_file_path} (5 lines total)]\n'
  61. '1|Line 1\n'
  62. '2| Line 2\n'
  63. '3|Line 3\n'
  64. '4|Line 4\n'
  65. '5|Line 5\n'
  66. )
  67. assert result.split('\n') == expected.split('\n')
  68. def test_open_file_long(tmp_path):
  69. temp_file_path = tmp_path / 'a.txt'
  70. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  71. temp_file_path.write_text(content)
  72. with io.StringIO() as buf:
  73. with contextlib.redirect_stdout(buf):
  74. open_file(str(temp_file_path))
  75. result = buf.getvalue()
  76. assert result is not None
  77. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  78. for i in range(1, 52):
  79. expected += f'{i}|Line {i}\n'
  80. expected += '(949 more lines below)\n'
  81. assert result.split('\n') == expected.split('\n')
  82. def test_open_file_long_with_lineno(tmp_path):
  83. temp_file_path = tmp_path / 'a.txt'
  84. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  85. temp_file_path.write_text(content)
  86. with io.StringIO() as buf:
  87. with contextlib.redirect_stdout(buf):
  88. open_file(str(temp_file_path), 100)
  89. result = buf.getvalue()
  90. assert result is not None
  91. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  92. expected += '(50 more lines above)\n'
  93. for i in range(51, 151):
  94. expected += f'{i}|Line {i}\n'
  95. expected += '(850 more lines below)\n'
  96. assert result.split('\n') == expected.split('\n')
  97. def test_create_file_unexist_path():
  98. with pytest.raises(FileNotFoundError):
  99. create_file('/unexist/path/a.txt')
  100. def test_create_file(tmp_path):
  101. temp_file_path = tmp_path / 'a.txt'
  102. with io.StringIO() as buf:
  103. with contextlib.redirect_stdout(buf):
  104. create_file(str(temp_file_path))
  105. result = buf.getvalue()
  106. expected = (
  107. f'[File: {temp_file_path} (1 lines total)]\n'
  108. '1|\n'
  109. f'[File {temp_file_path} created.]\n'
  110. )
  111. assert result.split('\n') == expected.split('\n')
  112. def test_goto_line(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))
  119. result = buf.getvalue()
  120. assert result is not None
  121. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  122. for i in range(1, 52):
  123. expected += f'{i}|Line {i}\n'
  124. expected += '(949 more lines below)\n'
  125. assert result.split('\n') == expected.split('\n')
  126. with io.StringIO() as buf:
  127. with contextlib.redirect_stdout(buf):
  128. goto_line(100)
  129. result = buf.getvalue()
  130. assert result is not None
  131. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  132. expected += '(50 more lines above)\n'
  133. for i in range(51, 151):
  134. expected += f'{i}|Line {i}\n'
  135. expected += '(850 more lines below)\n'
  136. assert result.split('\n') == expected.split('\n')
  137. def test_goto_line_negative(tmp_path):
  138. temp_file_path = tmp_path / 'a.txt'
  139. content = '\n'.join([f'Line {i}' for i in range(1, 5)])
  140. temp_file_path.write_text(content)
  141. with io.StringIO() as buf:
  142. with contextlib.redirect_stdout(buf):
  143. open_file(str(temp_file_path))
  144. with pytest.raises(ValueError):
  145. goto_line(-1)
  146. def test_goto_line_out_of_bound(tmp_path):
  147. temp_file_path = tmp_path / 'a.txt'
  148. content = '\n'.join([f'Line {i}' for i in range(1, 5)])
  149. temp_file_path.write_text(content)
  150. with io.StringIO() as buf:
  151. with contextlib.redirect_stdout(buf):
  152. open_file(str(temp_file_path))
  153. with pytest.raises(ValueError):
  154. goto_line(100)
  155. def test_scroll_down(tmp_path):
  156. temp_file_path = tmp_path / 'a.txt'
  157. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  158. temp_file_path.write_text(content)
  159. with io.StringIO() as buf:
  160. with contextlib.redirect_stdout(buf):
  161. open_file(str(temp_file_path))
  162. result = buf.getvalue()
  163. assert result is not None
  164. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  165. for i in range(1, 52):
  166. expected += f'{i}|Line {i}\n'
  167. expected += '(949 more lines below)\n'
  168. assert result.split('\n') == expected.split('\n')
  169. with io.StringIO() as buf:
  170. with contextlib.redirect_stdout(buf):
  171. scroll_down()
  172. result = buf.getvalue()
  173. assert result is not None
  174. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  175. expected += '(51 more lines above)\n'
  176. for i in range(52, 152):
  177. expected += f'{i}|Line {i}\n'
  178. expected += '(849 more lines below)\n'
  179. assert result.split('\n') == expected.split('\n')
  180. def test_scroll_up(tmp_path):
  181. temp_file_path = tmp_path / 'a.txt'
  182. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  183. temp_file_path.write_text(content)
  184. with io.StringIO() as buf:
  185. with contextlib.redirect_stdout(buf):
  186. open_file(str(temp_file_path), 300)
  187. result = buf.getvalue()
  188. assert result is not None
  189. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  190. expected += '(250 more lines above)\n'
  191. for i in range(251, 351):
  192. expected += f'{i}|Line {i}\n'
  193. expected += '(650 more lines below)\n'
  194. assert result.split('\n') == expected.split('\n')
  195. with io.StringIO() as buf:
  196. with contextlib.redirect_stdout(buf):
  197. scroll_up()
  198. result = buf.getvalue()
  199. assert result is not None
  200. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  201. expected += '(150 more lines above)\n'
  202. for i in range(151, 251):
  203. expected += f'{i}|Line {i}\n'
  204. expected += '(750 more lines below)\n'
  205. assert result.split('\n') == expected.split('\n')
  206. def test_scroll_down_edge(tmp_path):
  207. temp_file_path = tmp_path / 'a.txt'
  208. content = '\n'.join([f'Line {i}' for i in range(1, 10)])
  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. result = buf.getvalue()
  214. assert result is not None
  215. expected = f'[File: {temp_file_path} (9 lines total)]\n'
  216. for i in range(1, 10):
  217. expected += f'{i}|Line {i}\n'
  218. with io.StringIO() as buf:
  219. with contextlib.redirect_stdout(buf):
  220. scroll_down()
  221. result = buf.getvalue()
  222. assert result is not None
  223. # expected should be unchanged
  224. assert result.split('\n') == expected.split('\n')
  225. def test_print_window_internal(tmp_path):
  226. test_file_path = tmp_path / 'a.txt'
  227. create_file(str(test_file_path))
  228. open_file(str(test_file_path))
  229. with open(test_file_path, 'w') as file:
  230. for i in range(1, 101):
  231. file.write(f'Line `{i}`\n')
  232. # Define the parameters for the test
  233. current_line = 50
  234. window = 2
  235. # Test _print_window especially with backticks
  236. with io.StringIO() as buf:
  237. with contextlib.redirect_stdout(buf):
  238. _print_window(str(test_file_path), current_line, window, return_str=False)
  239. result = buf.getvalue()
  240. expected = (
  241. '(49 more lines above)\n'
  242. '50|Line `50`\n'
  243. '51|Line `51`\n'
  244. '(49 more lines below)\n'
  245. )
  246. assert result == expected
  247. def test_edit_file_window(tmp_path, monkeypatch):
  248. # Set environment variable via monkeypatch does NOT work!
  249. monkeypatch.setattr(
  250. 'opendevin.runtime.plugins.agent_skills.agentskills.ENABLE_AUTO_LINT', True
  251. )
  252. content = """def any_int(a, b, c):
  253. return isinstance(a, int) and isinstance(b, int) and isinstance(c, int)
  254. def test_any_int():
  255. assert any_int(1, 2, 3) == True
  256. assert any_int(1.5, 2, 3) == False
  257. assert any_int(1, 2.5, 3) == False
  258. assert any_int(1, 2, 3.5) == False
  259. assert any_int(1.0, 2, 3) == False
  260. assert any_int(1, 2.0, 3) == False
  261. assert any_int(1, 2, 3.0) == False
  262. assert any_int(0, 0, 0) == True
  263. assert any_int(-1, -2, -3) == True
  264. assert any_int(1, -2, 3) == True
  265. assert any_int(1.5, -2, 3) == False
  266. assert any_int(1, -2.5, 3) == False
  267. def check(any_int):
  268. # Check some simple cases
  269. assert any_int(2, 3, 1)==True, "This prints if this assert fails 1 (good for debugging!)"
  270. assert any_int(2.5, 2, 3)==False, "This prints if this assert fails 2 (good for debugging!)"
  271. assert any_int(1.5, 5, 3.5)==False, "This prints if this assert fails 3 (good for debugging!)"
  272. assert any_int(2, 6, 2)==False, "This prints if this assert fails 4 (good for debugging!)"
  273. assert any_int(4, 2, 2)==True, "This prints if this assert fails 5 (good for debugging!)"
  274. assert any_int(2.2, 2.2, 2.2)==False, "This prints if this assert fails 6 (good for debugging!)"
  275. assert any_int(-4, 6, 2)==True, "This prints if this assert fails 7 (good for debugging!)"
  276. # Check some edge cases that are easy to work out by hand.
  277. assert any_int(2,1,1)==True, "This prints if this assert fails 8 (also good for debugging!)"
  278. assert any_int(3,4,7)==True, "This prints if this assert fails 9 (also good for debugging!)"
  279. assert any_int(3.0,4,7)==False, "This prints if this assert fails 10 (also good for debugging!)"
  280. check(any_int)"""
  281. temp_file_path = tmp_path / 'error-test.py'
  282. temp_file_path.write_text(content)
  283. open_file(str(temp_file_path))
  284. with io.StringIO() as buf:
  285. with contextlib.redirect_stdout(buf):
  286. edit_file(
  287. str(temp_file_path),
  288. start=9,
  289. end=9,
  290. content=' assert any_int(1.0, 2, 3) == False',
  291. )
  292. result = buf.getvalue()
  293. expected = (
  294. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  295. 'ERRORS:\n'
  296. + str(temp_file_path)
  297. + ':9:9: '
  298. + 'E999 IndentationError: unexpected indent\n'
  299. '[This is how your edit would have looked if applied]\n'
  300. '-------------------------------------------------\n'
  301. '(4 more lines above)\n'
  302. '5| assert any_int(1, 2, 3) == True\n'
  303. '6| assert any_int(1.5, 2, 3) == False\n'
  304. '7| assert any_int(1, 2.5, 3) == False\n'
  305. '8| assert any_int(1, 2, 3.5) == False\n'
  306. '9| assert any_int(1.0, 2, 3) == False\n'
  307. '10| assert any_int(1, 2.0, 3) == False\n'
  308. '11| assert any_int(1, 2, 3.0) == False\n'
  309. '12| assert any_int(0, 0, 0) == True\n'
  310. '13| assert any_int(-1, -2, -3) == True\n'
  311. '14| assert any_int(1, -2, 3) == True\n'
  312. '(19 more lines below)\n'
  313. '-------------------------------------------------\n'
  314. '\n'
  315. '[This is the original code before your edit]\n'
  316. '-------------------------------------------------\n'
  317. '(4 more lines above)\n'
  318. '5| assert any_int(1, 2, 3) == True\n'
  319. '6| assert any_int(1.5, 2, 3) == False\n'
  320. '7| assert any_int(1, 2.5, 3) == False\n'
  321. '8| assert any_int(1, 2, 3.5) == False\n'
  322. '9| assert any_int(1.0, 2, 3) == False\n'
  323. '10| assert any_int(1, 2.0, 3) == False\n'
  324. '11| assert any_int(1, 2, 3.0) == False\n'
  325. '12| assert any_int(0, 0, 0) == True\n'
  326. '13| assert any_int(-1, -2, -3) == True\n'
  327. '14| assert any_int(1, -2, 3) == True\n'
  328. '(19 more lines below)\n'
  329. '-------------------------------------------------\n'
  330. 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  331. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  332. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  333. )
  334. assert result == expected
  335. def test_append_file(tmp_path):
  336. temp_file_path = tmp_path / 'a.txt'
  337. content = 'Line 1\nLine 2'
  338. temp_file_path.write_text(content)
  339. open_file(str(temp_file_path))
  340. with io.StringIO() as buf:
  341. with contextlib.redirect_stdout(buf):
  342. append_file(str(temp_file_path), content='APPENDED TEXT')
  343. result = buf.getvalue()
  344. expected = (
  345. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  346. '1|Line 1\n'
  347. '2|Line 2\n'
  348. '3|APPENDED TEXT\n'
  349. '[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]\n'
  350. )
  351. assert result.split('\n') == expected.split('\n')
  352. with open(temp_file_path, 'r') as file:
  353. lines = file.readlines()
  354. assert len(lines) == 3
  355. assert lines[0].rstrip() == 'Line 1'
  356. assert lines[1].rstrip() == 'Line 2'
  357. assert lines[2].rstrip() == 'APPENDED TEXT'
  358. def test_append_file_from_scratch(tmp_path):
  359. temp_file_path = tmp_path / 'a.txt'
  360. create_file(str(temp_file_path))
  361. try:
  362. open_file(str(temp_file_path))
  363. with io.StringIO() as buf:
  364. with contextlib.redirect_stdout(buf):
  365. append_file(str(temp_file_path), content='APPENDED TEXT')
  366. result = buf.getvalue()
  367. expected = (
  368. f'[File: {temp_file_path} (1 lines total after edit)]\n'
  369. '1|APPENDED TEXT\n'
  370. '[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]\n'
  371. )
  372. assert result.split('\n') == expected.split('\n')
  373. with open(temp_file_path, 'r') as file:
  374. lines = file.readlines()
  375. assert len(lines) == 1
  376. assert lines[0].rstrip() == 'APPENDED TEXT'
  377. finally:
  378. os.remove(temp_file_path)
  379. def test_append_file_from_scratch_multiline(tmp_path):
  380. temp_file_path = tmp_path / 'a3.txt'
  381. create_file(str(temp_file_path))
  382. try:
  383. open_file(temp_file_path)
  384. with io.StringIO() as buf:
  385. with contextlib.redirect_stdout(buf):
  386. append_file(
  387. str(temp_file_path),
  388. content='APPENDED TEXT1\nAPPENDED TEXT2\nAPPENDED TEXT3',
  389. )
  390. result = buf.getvalue()
  391. expected = (
  392. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  393. '1|APPENDED TEXT1\n'
  394. '2|APPENDED TEXT2\n'
  395. '3|APPENDED TEXT3\n'
  396. '[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]\n'
  397. )
  398. assert result.split('\n') == expected.split('\n')
  399. with open(temp_file_path, 'r') as file:
  400. lines = file.readlines()
  401. assert len(lines) == 3
  402. assert lines[0].rstrip() == 'APPENDED TEXT1'
  403. assert lines[1].rstrip() == 'APPENDED TEXT2'
  404. assert lines[2].rstrip() == 'APPENDED TEXT3'
  405. finally:
  406. os.remove(temp_file_path)
  407. def test_append_file_not_opened():
  408. with pytest.raises(FileNotFoundError):
  409. append_file(str('unknown file'), content='APPEND TEXT')
  410. def test_edit_file(tmp_path):
  411. temp_file_path = tmp_path / 'a.txt'
  412. content = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'
  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(
  418. str(temp_file_path),
  419. start=1,
  420. end=3,
  421. content='REPLACE TEXT',
  422. )
  423. result = buf.getvalue()
  424. expected = (
  425. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  426. '1|REPLACE TEXT\n'
  427. '2|Line 4\n'
  428. '3|Line 5\n' + MSG_FILE_UPDATED + '\n'
  429. )
  430. assert result.split('\n') == expected.split('\n')
  431. with open(temp_file_path, 'r') as file:
  432. lines = file.readlines()
  433. assert len(lines) == 3
  434. assert lines[0].rstrip() == 'REPLACE TEXT'
  435. assert lines[1].rstrip() == 'Line 4'
  436. assert lines[2].rstrip() == 'Line 5'
  437. def test_edit_file_from_scratch(tmp_path):
  438. temp_file_path = tmp_path / 'a.txt'
  439. create_file(str(temp_file_path))
  440. open_file(str(temp_file_path))
  441. with io.StringIO() as buf:
  442. with contextlib.redirect_stdout(buf):
  443. edit_file(
  444. str(temp_file_path),
  445. start=1,
  446. end=1,
  447. content='REPLACE TEXT',
  448. )
  449. result = buf.getvalue()
  450. expected = (
  451. f'[File: {temp_file_path} (1 lines total after edit)]\n'
  452. '1|REPLACE TEXT\n' + MSG_FILE_UPDATED + '\n'
  453. )
  454. assert result.split('\n') == expected.split('\n')
  455. with open(temp_file_path, 'r') as file:
  456. lines = file.readlines()
  457. assert len(lines) == 1
  458. assert lines[0].rstrip() == 'REPLACE TEXT'
  459. def test_edit_file_from_scratch_multiline_with_backticks_and_second_edit(tmp_path):
  460. temp_file_path = tmp_path / 'a.txt'
  461. create_file(str(temp_file_path))
  462. open_file(str(temp_file_path))
  463. with io.StringIO() as buf:
  464. with contextlib.redirect_stdout(buf):
  465. edit_file(
  466. str(temp_file_path),
  467. 1,
  468. 1,
  469. '`REPLACE TEXT1`\n`REPLACE TEXT2`\n`REPLACE TEXT3`',
  470. )
  471. result = buf.getvalue()
  472. expected = (
  473. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  474. '1|`REPLACE TEXT1`\n'
  475. '2|`REPLACE TEXT2`\n'
  476. '3|`REPLACE TEXT3`\n' + MSG_FILE_UPDATED + '\n'
  477. )
  478. assert result.split('\n') == expected.split('\n')
  479. with open(temp_file_path, 'r') as file:
  480. lines = file.readlines()
  481. assert len(lines) == 3
  482. assert lines[0].rstrip() == '`REPLACE TEXT1`'
  483. assert lines[1].rstrip() == '`REPLACE TEXT2`'
  484. assert lines[2].rstrip() == '`REPLACE TEXT3`'
  485. # Check that no backticks are escaped in the edit_file call
  486. assert '\\`' not in result
  487. # Perform a second edit
  488. with io.StringIO() as buf:
  489. with contextlib.redirect_stdout(buf):
  490. edit_file(
  491. str(temp_file_path),
  492. 1,
  493. 3,
  494. '`REPLACED TEXT1`\n`REPLACED TEXT2`\n`REPLACED TEXT3`',
  495. )
  496. second_result = buf.getvalue()
  497. second_expected = (
  498. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  499. '1|`REPLACED TEXT1`\n'
  500. '2|`REPLACED TEXT2`\n'
  501. '3|`REPLACED TEXT3`\n' + MSG_FILE_UPDATED + '\n'
  502. )
  503. assert second_result.split('\n') == second_expected.split('\n')
  504. with open(temp_file_path, 'r') as file:
  505. lines = file.readlines()
  506. assert len(lines) == 3
  507. assert lines[0].rstrip() == '`REPLACED TEXT1`'
  508. assert lines[1].rstrip() == '`REPLACED TEXT2`'
  509. assert lines[2].rstrip() == '`REPLACED TEXT3`'
  510. # Check that no backticks are escaped in the second edit_file call
  511. assert '\\`' not in second_result
  512. def test_edit_file_from_scratch_multiline(tmp_path):
  513. temp_file_path = tmp_path / 'a.txt'
  514. create_file(str(temp_file_path))
  515. open_file(temp_file_path)
  516. with io.StringIO() as buf:
  517. with contextlib.redirect_stdout(buf):
  518. edit_file(
  519. str(temp_file_path),
  520. 1,
  521. 1,
  522. content='REPLACE TEXT1\nREPLACE TEXT2\nREPLACE TEXT3',
  523. )
  524. result = buf.getvalue()
  525. expected = (
  526. f'[File: {temp_file_path} (3 lines total after edit)]\n'
  527. '1|REPLACE TEXT1\n'
  528. '2|REPLACE TEXT2\n'
  529. '3|REPLACE TEXT3\n' + MSG_FILE_UPDATED + '\n'
  530. )
  531. assert result.split('\n') == expected.split('\n')
  532. with open(temp_file_path, 'r') as file:
  533. lines = file.readlines()
  534. assert len(lines) == 3
  535. assert lines[0].rstrip() == 'REPLACE TEXT1'
  536. assert lines[1].rstrip() == 'REPLACE TEXT2'
  537. assert lines[2].rstrip() == 'REPLACE TEXT3'
  538. def test_edit_file_not_opened():
  539. with pytest.raises(FileNotFoundError):
  540. edit_file(
  541. str('unknown file'),
  542. 1,
  543. 3,
  544. 'REPLACE TEXT',
  545. )
  546. def test_search_dir(tmp_path):
  547. # create files with the search term "bingo"
  548. for i in range(1, 101):
  549. temp_file_path = tmp_path / f'a{i}.txt'
  550. with open(temp_file_path, 'w') as file:
  551. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  552. if i == 50:
  553. file.write('bingo')
  554. # test
  555. with io.StringIO() as buf:
  556. with contextlib.redirect_stdout(buf):
  557. search_dir('bingo', str(tmp_path))
  558. result = buf.getvalue()
  559. assert result is not None
  560. expected = (
  561. f'[Found 1 matches for "bingo" in {tmp_path}]\n'
  562. f'{tmp_path}/a50.txt (Line 6): bingo\n'
  563. f'[End of matches for "bingo" in {tmp_path}]\n'
  564. )
  565. assert result.split('\n') == expected.split('\n')
  566. def test_search_dir_not_exist_term(tmp_path):
  567. # create files with the search term "bingo"
  568. for i in range(1, 101):
  569. temp_file_path = tmp_path / f'a{i}.txt'
  570. with open(temp_file_path, 'w') as file:
  571. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  572. # test
  573. with io.StringIO() as buf:
  574. with contextlib.redirect_stdout(buf):
  575. search_dir('non-exist', str(tmp_path))
  576. result = buf.getvalue()
  577. assert result is not None
  578. expected = f'No matches found for "non-exist" in {tmp_path}\n'
  579. assert result.split('\n') == expected.split('\n')
  580. def test_search_dir_too_much_match(tmp_path):
  581. # create files with the search term "Line 5"
  582. for i in range(1, 1000):
  583. temp_file_path = tmp_path / f'a{i}.txt'
  584. with open(temp_file_path, 'w') as file:
  585. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  586. with io.StringIO() as buf:
  587. with contextlib.redirect_stdout(buf):
  588. search_dir('Line 5', str(tmp_path))
  589. result = buf.getvalue()
  590. assert result is not None
  591. expected = f'More than 999 files matched for "Line 5" in {tmp_path}. Please narrow your search.\n'
  592. assert result.split('\n') == expected.split('\n')
  593. def test_search_dir_cwd(tmp_path, monkeypatch):
  594. # Using pytest's monkeypatch to change directory without affecting other tests
  595. monkeypatch.chdir(tmp_path)
  596. # create files with the search term "bingo"
  597. for i in range(1, 101):
  598. temp_file_path = tmp_path / f'a{i}.txt'
  599. with open(temp_file_path, 'w') as file:
  600. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  601. if i == 50:
  602. file.write('bingo')
  603. with io.StringIO() as buf:
  604. with contextlib.redirect_stdout(buf):
  605. search_dir('bingo')
  606. result = buf.getvalue()
  607. assert result is not None
  608. expected = (
  609. '[Found 1 matches for "bingo" in ./]\n'
  610. './a50.txt (Line 6): bingo\n'
  611. '[End of matches for "bingo" in ./]\n'
  612. )
  613. assert result.split('\n') == expected.split('\n')
  614. def test_search_file(tmp_path):
  615. temp_file_path = tmp_path / 'a.txt'
  616. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  617. with io.StringIO() as buf:
  618. with contextlib.redirect_stdout(buf):
  619. search_file('Line 5', str(temp_file_path))
  620. result = buf.getvalue()
  621. assert result is not None
  622. expected = f'[Found 1 matches for "Line 5" in {temp_file_path}]\n'
  623. expected += 'Line 5: Line 5\n'
  624. expected += f'[End of matches for "Line 5" in {temp_file_path}]\n'
  625. assert result.split('\n') == expected.split('\n')
  626. def test_search_file_not_exist_term(tmp_path):
  627. temp_file_path = tmp_path / 'a.txt'
  628. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  629. with io.StringIO() as buf:
  630. with contextlib.redirect_stdout(buf):
  631. search_file('Line 6', str(temp_file_path))
  632. result = buf.getvalue()
  633. assert result is not None
  634. expected = f'[No matches found for "Line 6" in {temp_file_path}]\n'
  635. assert result.split('\n') == expected.split('\n')
  636. def test_search_file_not_exist_file():
  637. with pytest.raises(FileNotFoundError):
  638. search_file('Line 6', '/unexist/path/a.txt')
  639. def test_find_file(tmp_path):
  640. temp_file_path = tmp_path / 'a.txt'
  641. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  642. with io.StringIO() as buf:
  643. with contextlib.redirect_stdout(buf):
  644. find_file('a.txt', str(tmp_path))
  645. result = buf.getvalue()
  646. assert result is not None
  647. expected = f'[Found 1 matches for "a.txt" in {tmp_path}]\n'
  648. expected += f'{tmp_path}/a.txt\n'
  649. expected += f'[End of matches for "a.txt" in {tmp_path}]\n'
  650. assert result.split('\n') == expected.split('\n')
  651. def test_find_file_cwd(tmp_path, monkeypatch):
  652. monkeypatch.chdir(tmp_path)
  653. temp_file_path = tmp_path / 'a.txt'
  654. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  655. with io.StringIO() as buf:
  656. with contextlib.redirect_stdout(buf):
  657. find_file('a.txt')
  658. result = buf.getvalue()
  659. assert result is not None
  660. def test_find_file_not_exist_file():
  661. with io.StringIO() as buf:
  662. with contextlib.redirect_stdout(buf):
  663. find_file('unexist.txt')
  664. result = buf.getvalue()
  665. assert result is not None
  666. expected = '[No matches found for "unexist.txt" in ./]\n'
  667. assert result.split('\n') == expected.split('\n')
  668. def test_find_file_not_exist_file_specific_path(tmp_path):
  669. with io.StringIO() as buf:
  670. with contextlib.redirect_stdout(buf):
  671. find_file('unexist.txt', str(tmp_path))
  672. result = buf.getvalue()
  673. assert result is not None
  674. expected = f'[No matches found for "unexist.txt" in {tmp_path}]\n'
  675. assert result.split('\n') == expected.split('\n')
  676. def test_edit_lint_file_pass(tmp_path, monkeypatch):
  677. # Create a Python file with correct syntax
  678. file_path = tmp_path / 'test_file.py'
  679. file_path.write_text('\n')
  680. # patch ENABLE_AUTO_LINT
  681. monkeypatch.setattr(
  682. 'opendevin.runtime.plugins.agent_skills.agentskills.ENABLE_AUTO_LINT', True
  683. )
  684. # Test linting functionality
  685. with io.StringIO() as buf:
  686. with contextlib.redirect_stdout(buf):
  687. open_file(str(file_path))
  688. edit_file(str(file_path), 1, 1, "print('hello')\n")
  689. result = buf.getvalue()
  690. assert result is not None
  691. expected = (
  692. f'[File: {file_path} (1 lines total)]\n'
  693. '1|\n'
  694. f'[File: {file_path} (2 lines total after edit)]\n'
  695. "1|print('hello')\n"
  696. '2|\n' + MSG_FILE_UPDATED + '\n'
  697. )
  698. assert result.split('\n') == expected.split('\n')
  699. def test_lint_file_fail_undefined_name(tmp_path, monkeypatch, capsys):
  700. # Create a Python file with a syntax error
  701. file_path = tmp_path / 'test_file.py'
  702. file_path.write_text('\n')
  703. # Set environment variable to enable linting
  704. monkeypatch.setattr(
  705. 'opendevin.runtime.plugins.agent_skills.agentskills.ENABLE_AUTO_LINT', True
  706. )
  707. open_file(str(file_path))
  708. edit_file(str(file_path), 1, 1, 'undefined_name()\n')
  709. result = capsys.readouterr().out
  710. print(result)
  711. assert result is not None
  712. expected = (
  713. f'[File: {file_path} (1 lines total)]\n'
  714. '1|\n'
  715. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  716. 'ERRORS:\n'
  717. f"{file_path}:1:1: F821 undefined name 'undefined_name'\n"
  718. '[This is how your edit would have looked if applied]\n'
  719. '-------------------------------------------------\n'
  720. '1|undefined_name()\n'
  721. '2|\n'
  722. '-------------------------------------------------\n\n'
  723. '[This is the original code before your edit]\n'
  724. '-------------------------------------------------\n'
  725. '1|\n'
  726. '-------------------------------------------------\n'
  727. 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  728. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  729. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  730. )
  731. assert result.split('\n') == expected.split('\n')
  732. def test_lint_file_fail_undefined_name_long(tmp_path, monkeypatch, capsys):
  733. # Create a Python file with a syntax error
  734. file_path = tmp_path / 'test_file.py'
  735. file_path.write_text('\n' * 1000)
  736. # Set environment variable to enable linting
  737. monkeypatch.setattr(
  738. 'opendevin.runtime.plugins.agent_skills.agentskills.ENABLE_AUTO_LINT', True
  739. )
  740. open_file(str(file_path))
  741. edit_file(str(file_path), 500, 500, 'undefined_name()\n')
  742. result = capsys.readouterr().out
  743. print(result)
  744. assert result is not None
  745. open_lines = '\n'.join([f'{i+1}|' for i in range(51)])
  746. expected = (
  747. f'[File: {file_path} (1000 lines total)]\n'
  748. f'{open_lines}\n'
  749. '(949 more lines below)\n'
  750. '[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]\n'
  751. 'ERRORS:\n'
  752. f"{file_path}:500:1: F821 undefined name 'undefined_name'\n"
  753. '[This is how your edit would have looked if applied]\n'
  754. '-------------------------------------------------\n'
  755. '(495 more lines above)\n'
  756. '496|\n'
  757. '497|\n'
  758. '498|\n'
  759. '499|\n'
  760. '500|undefined_name()\n'
  761. '501|\n'
  762. '502|\n'
  763. '503|\n'
  764. '504|\n'
  765. '505|\n'
  766. '(496 more lines below)\n'
  767. '-------------------------------------------------\n\n'
  768. '[This is the original code before your edit]\n'
  769. '-------------------------------------------------\n'
  770. '(495 more lines above)\n'
  771. '496|\n'
  772. '497|\n'
  773. '498|\n'
  774. '499|\n'
  775. '500|\n'
  776. '501|\n'
  777. '502|\n'
  778. '503|\n'
  779. '504|\n'
  780. '505|\n'
  781. '(495 more lines below)\n'
  782. '-------------------------------------------------\n'
  783. 'Your changes have NOT been applied. Please fix your edit command and try again.\n'
  784. 'You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.\n'
  785. 'DO NOT re-run the same failed edit command. Running it again will lead to the same error.\n'
  786. )
  787. assert result.split('\n') == expected.split('\n')
  788. def test_lint_file_disabled_undefined_name(tmp_path, monkeypatch, capsys):
  789. # Create a Python file with a syntax error
  790. file_path = tmp_path / 'test_file.py'
  791. file_path.write_text('\n')
  792. # Set environment variable to disable linting
  793. monkeypatch.setattr(
  794. 'opendevin.runtime.plugins.agent_skills.agentskills.ENABLE_AUTO_LINT', False
  795. )
  796. open_file(str(file_path))
  797. edit_file(str(file_path), 1, 1, 'undefined_name()\n')
  798. result = capsys.readouterr().out
  799. assert result is not None
  800. expected = (
  801. f'[File: {file_path} (1 lines total)]\n'
  802. '1|\n'
  803. f'[File: {file_path} (2 lines total after edit)]\n'
  804. '1|undefined_name()\n'
  805. '2|\n' + MSG_FILE_UPDATED + '\n'
  806. )
  807. assert result.split('\n') == expected.split('\n')
  808. def test_parse_docx(tmp_path):
  809. # Create a DOCX file with some content
  810. test_docx_path = tmp_path / 'test.docx'
  811. doc = docx.Document()
  812. doc.add_paragraph('Hello, this is a test document.')
  813. doc.add_paragraph('This is the second paragraph.')
  814. doc.save(str(test_docx_path))
  815. old_stdout = sys.stdout
  816. sys.stdout = io.StringIO()
  817. # Call the parse_docx function
  818. parse_docx(str(test_docx_path))
  819. # Capture the output
  820. output = sys.stdout.getvalue()
  821. sys.stdout = old_stdout
  822. # Check if the output is correct
  823. expected_output = (
  824. f'[Reading DOCX file from {test_docx_path}]\n'
  825. '@@ Page 1 @@\nHello, this is a test document.\n\n'
  826. '@@ Page 2 @@\nThis is the second paragraph.\n\n\n'
  827. )
  828. assert output == expected_output, f'Expected output does not match. Got: {output}'
  829. def test_parse_latex(tmp_path):
  830. # Create a LaTeX file with some content
  831. test_latex_path = tmp_path / 'test.tex'
  832. with open(test_latex_path, 'w') as f:
  833. f.write(r"""
  834. \documentclass{article}
  835. \begin{document}
  836. Hello, this is a test LaTeX document.
  837. \end{document}
  838. """)
  839. old_stdout = sys.stdout
  840. sys.stdout = io.StringIO()
  841. # Call the parse_latex function
  842. parse_latex(str(test_latex_path))
  843. # Capture the output
  844. output = sys.stdout.getvalue()
  845. sys.stdout = old_stdout
  846. # Check if the output is correct
  847. expected_output = (
  848. f'[Reading LaTex file from {test_latex_path}]\n'
  849. 'Hello, this is a test LaTeX document.\n'
  850. )
  851. assert output == expected_output, f'Expected output does not match. Got: {output}'
  852. def test_parse_pdf(tmp_path):
  853. # Create a PDF file with some content
  854. test_pdf_path = tmp_path / 'test.pdf'
  855. from reportlab.lib.pagesizes import letter
  856. from reportlab.pdfgen import canvas
  857. c = canvas.Canvas(str(test_pdf_path), pagesize=letter)
  858. c.drawString(100, 750, 'Hello, this is a test PDF document.')
  859. c.save()
  860. old_stdout = sys.stdout
  861. sys.stdout = io.StringIO()
  862. # Call the parse_pdf function
  863. parse_pdf(str(test_pdf_path))
  864. # Capture the output
  865. output = sys.stdout.getvalue()
  866. sys.stdout = old_stdout
  867. # Check if the output is correct
  868. expected_output = (
  869. f'[Reading PDF file from {test_pdf_path}]\n'
  870. '@@ Page 1 @@\n'
  871. 'Hello, this is a test PDF document.\n'
  872. )
  873. assert output == expected_output, f'Expected output does not match. Got: {output}'
  874. def test_parse_pptx(tmp_path):
  875. test_pptx_path = tmp_path / 'test.pptx'
  876. from pptx import Presentation
  877. pres = Presentation()
  878. slide1 = pres.slides.add_slide(pres.slide_layouts[0])
  879. title1 = slide1.shapes.title
  880. title1.text = 'Hello, this is the first test PPTX slide.'
  881. slide2 = pres.slides.add_slide(pres.slide_layouts[0])
  882. title2 = slide2.shapes.title
  883. title2.text = 'Hello, this is the second test PPTX slide.'
  884. pres.save(str(test_pptx_path))
  885. old_stdout = sys.stdout
  886. sys.stdout = io.StringIO()
  887. parse_pptx(str(test_pptx_path))
  888. output = sys.stdout.getvalue()
  889. sys.stdout = old_stdout
  890. expected_output = (
  891. f'[Reading PowerPoint file from {test_pptx_path}]\n'
  892. '@@ Slide 1 @@\n'
  893. 'Hello, this is the first test PPTX slide.\n\n'
  894. '@@ Slide 2 @@\n'
  895. 'Hello, this is the second test PPTX slide.\n\n'
  896. )
  897. assert output == expected_output, f'Expected output does not match. Got: {output}'