test_agent_skill.py 38 KB

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