test_agent_skill.py 36 KB

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