test_agent_skill.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. import contextlib
  2. import io
  3. import sys
  4. import docx
  5. import pytest
  6. from openhands.runtime.plugins.agent_skills.agentskills import file_editor
  7. from openhands.runtime.plugins.agent_skills.file_ops.file_ops import (
  8. WINDOW,
  9. _print_window,
  10. find_file,
  11. goto_line,
  12. open_file,
  13. scroll_down,
  14. scroll_up,
  15. search_dir,
  16. search_file,
  17. )
  18. from openhands.runtime.plugins.agent_skills.file_reader.file_readers import (
  19. parse_docx,
  20. parse_latex,
  21. parse_pdf,
  22. parse_pptx,
  23. )
  24. # CURRENT_FILE must be reset for each test
  25. @pytest.fixture(autouse=True)
  26. def reset_current_file():
  27. from openhands.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 _generate_ruby_test_file_with_lines(temp_path, num_lines) -> str:
  36. file_path = temp_path / 'test_file.rb'
  37. file_path.write_text('\n' * num_lines)
  38. return file_path
  39. def _calculate_window_bounds(current_line, total_lines, window_size):
  40. """Calculate the bounds of the window around the current line."""
  41. half_window = window_size // 2
  42. if current_line - half_window < 0:
  43. start = 1
  44. end = window_size
  45. else:
  46. start = current_line - half_window
  47. end = current_line + half_window
  48. return start, end
  49. def _capture_file_operation_error(operation, expected_error_msg):
  50. with io.StringIO() as buf:
  51. with contextlib.redirect_stdout(buf):
  52. operation()
  53. result = buf.getvalue().strip()
  54. assert result == expected_error_msg
  55. SEP = '-' * 49 + '\n'
  56. # =============================================================================
  57. def test_open_file_unexist_path():
  58. _capture_file_operation_error(
  59. lambda: open_file('/unexist/path/a.txt'),
  60. 'ERROR: File /unexist/path/a.txt not found.',
  61. )
  62. def test_open_file(tmp_path):
  63. assert tmp_path is not None
  64. temp_file_path = tmp_path / 'a.txt'
  65. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  66. with io.StringIO() as buf:
  67. with contextlib.redirect_stdout(buf):
  68. open_file(str(temp_file_path))
  69. result = buf.getvalue()
  70. assert result is not None
  71. expected = (
  72. f'[File: {temp_file_path} (5 lines total)]\n'
  73. '(this is the beginning of the file)\n'
  74. '1|Line 1\n'
  75. '2|Line 2\n'
  76. '3|Line 3\n'
  77. '4|Line 4\n'
  78. '5|Line 5\n'
  79. '(this is the end of the file)\n'
  80. )
  81. assert result.split('\n') == expected.split('\n')
  82. def test_open_file_with_indentation(tmp_path):
  83. temp_file_path = tmp_path / 'a.txt'
  84. temp_file_path.write_text('Line 1\n Line 2\nLine 3\nLine 4\nLine 5')
  85. with io.StringIO() as buf:
  86. with contextlib.redirect_stdout(buf):
  87. open_file(str(temp_file_path))
  88. result = buf.getvalue()
  89. assert result is not None
  90. expected = (
  91. f'[File: {temp_file_path} (5 lines total)]\n'
  92. '(this is the beginning of the file)\n'
  93. '1|Line 1\n'
  94. '2| Line 2\n'
  95. '3|Line 3\n'
  96. '4|Line 4\n'
  97. '5|Line 5\n'
  98. '(this is the end of the file)\n'
  99. )
  100. assert result.split('\n') == expected.split('\n')
  101. def test_open_file_long(tmp_path):
  102. temp_file_path = tmp_path / 'a.txt'
  103. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  104. temp_file_path.write_text(content)
  105. with io.StringIO() as buf:
  106. with contextlib.redirect_stdout(buf):
  107. open_file(str(temp_file_path), 1, 50)
  108. result = buf.getvalue()
  109. assert result is not None
  110. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  111. expected += '(this is the beginning of the file)\n'
  112. for i in range(1, 51):
  113. expected += f'{i}|Line {i}\n'
  114. expected += '(950 more lines below)\n'
  115. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  116. assert result.split('\n') == expected.split('\n')
  117. def test_open_file_long_with_lineno(tmp_path):
  118. temp_file_path = tmp_path / 'a.txt'
  119. content = '\n'.join([f'Line {i}' for i in range(1, 1001)])
  120. temp_file_path.write_text(content)
  121. cur_line = 100
  122. with io.StringIO() as buf:
  123. with contextlib.redirect_stdout(buf):
  124. open_file(str(temp_file_path), cur_line)
  125. result = buf.getvalue()
  126. assert result is not None
  127. expected = f'[File: {temp_file_path} (1000 lines total)]\n'
  128. # since 100 is < WINDOW and 100 - WINDOW//2 < 0, so it should show all lines from 1 to WINDOW
  129. start, end = _calculate_window_bounds(cur_line, 1000, WINDOW)
  130. if start == 1:
  131. expected += '(this is the beginning of the file)\n'
  132. else:
  133. expected += f'({start - 1} more lines above)\n'
  134. for i in range(start, end + 1):
  135. expected += f'{i}|Line {i}\n'
  136. if end == 1000:
  137. expected += '(this is the end of the file)\n'
  138. else:
  139. expected += f'({1000 - end} more lines below)\n'
  140. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  141. assert result.split('\n') == expected.split('\n')
  142. def test_goto_line(tmp_path):
  143. temp_file_path = tmp_path / 'a.txt'
  144. total_lines = 1000
  145. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  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. result = buf.getvalue()
  151. assert result is not None
  152. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  153. expected += '(this is the beginning of the file)\n'
  154. for i in range(1, WINDOW + 1):
  155. expected += f'{i}|Line {i}\n'
  156. expected += f'({total_lines - WINDOW} more lines below)\n'
  157. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  158. assert result.split('\n') == expected.split('\n')
  159. with io.StringIO() as buf:
  160. with contextlib.redirect_stdout(buf):
  161. goto_line(500)
  162. result = buf.getvalue()
  163. assert result is not None
  164. cur_line = 500
  165. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  166. start, end = _calculate_window_bounds(cur_line, total_lines, WINDOW)
  167. if start == 1:
  168. expected += '(this is the beginning of the file)\n'
  169. else:
  170. expected += f'({start - 1} more lines above)\n'
  171. for i in range(start, end + 1):
  172. expected += f'{i}|Line {i}\n'
  173. if end == total_lines:
  174. expected += '(this is the end of the file)\n'
  175. else:
  176. expected += f'({total_lines - end} more lines below)\n'
  177. assert result.split('\n') == expected.split('\n')
  178. def test_goto_line_negative(tmp_path):
  179. temp_file_path = tmp_path / 'a.txt'
  180. content = '\n'.join([f'Line {i}' for i in range(1, 5)])
  181. temp_file_path.write_text(content)
  182. with io.StringIO() as buf:
  183. with contextlib.redirect_stdout(buf):
  184. open_file(str(temp_file_path))
  185. _capture_file_operation_error(
  186. lambda: goto_line(-1), 'ERROR: Line number must be between 1 and 4.'
  187. )
  188. def test_goto_line_out_of_bound(tmp_path):
  189. temp_file_path = tmp_path / 'a.txt'
  190. content = '\n'.join([f'Line {i}' for i in range(1, 10)])
  191. temp_file_path.write_text(content)
  192. with io.StringIO() as buf:
  193. with contextlib.redirect_stdout(buf):
  194. open_file(str(temp_file_path))
  195. _capture_file_operation_error(
  196. lambda: goto_line(100), 'ERROR: Line number must be between 1 and 9.'
  197. )
  198. def test_scroll_down(tmp_path):
  199. temp_file_path = tmp_path / 'a.txt'
  200. total_lines = 1000
  201. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  202. temp_file_path.write_text(content)
  203. with io.StringIO() as buf:
  204. with contextlib.redirect_stdout(buf):
  205. open_file(str(temp_file_path))
  206. result = buf.getvalue()
  207. assert result is not None
  208. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  209. start, end = _calculate_window_bounds(1, total_lines, WINDOW)
  210. if start == 1:
  211. expected += '(this is the beginning of the file)\n'
  212. else:
  213. expected += f'({start - 1} more lines above)\n'
  214. for i in range(start, end + 1):
  215. expected += f'{i}|Line {i}\n'
  216. if end == total_lines:
  217. expected += '(this is the end of the file)\n'
  218. else:
  219. expected += f'({total_lines - end} more lines below)\n'
  220. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  221. assert result.split('\n') == expected.split('\n')
  222. with io.StringIO() as buf:
  223. with contextlib.redirect_stdout(buf):
  224. scroll_down()
  225. result = buf.getvalue()
  226. assert result is not None
  227. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  228. start = WINDOW + 1
  229. end = 2 * WINDOW + 1
  230. if start == 1:
  231. expected += '(this is the beginning of the file)\n'
  232. else:
  233. expected += f'({start - 1} more lines above)\n'
  234. for i in range(start, end + 1):
  235. expected += f'{i}|Line {i}\n'
  236. if end == total_lines:
  237. expected += '(this is the end of the file)\n'
  238. else:
  239. expected += f'({total_lines - end} more lines below)\n'
  240. assert result.split('\n') == expected.split('\n')
  241. def test_scroll_up(tmp_path):
  242. temp_file_path = tmp_path / 'a.txt'
  243. total_lines = 1000
  244. content = '\n'.join([f'Line {i}' for i in range(1, total_lines + 1)])
  245. temp_file_path.write_text(content)
  246. cur_line = 300
  247. with io.StringIO() as buf:
  248. with contextlib.redirect_stdout(buf):
  249. open_file(str(temp_file_path), cur_line)
  250. result = buf.getvalue()
  251. assert result is not None
  252. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  253. start, end = _calculate_window_bounds(cur_line, total_lines, WINDOW)
  254. if start == 1:
  255. expected += '(this is the beginning of the file)\n'
  256. else:
  257. expected += f'({start - 1} more lines above)\n'
  258. for i in range(start, end + 1):
  259. expected += f'{i}|Line {i}\n'
  260. if end == total_lines:
  261. expected += '(this is the end of the file)\n'
  262. else:
  263. expected += f'({total_lines - end} more lines below)\n'
  264. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  265. assert result.split('\n') == expected.split('\n')
  266. with io.StringIO() as buf:
  267. with contextlib.redirect_stdout(buf):
  268. scroll_up()
  269. result = buf.getvalue()
  270. assert result is not None
  271. cur_line = cur_line - WINDOW
  272. expected = f'[File: {temp_file_path} ({total_lines} lines total)]\n'
  273. start = cur_line
  274. end = cur_line + WINDOW
  275. if start == 1:
  276. expected += '(this is the beginning of the file)\n'
  277. else:
  278. expected += f'({start - 1} more lines above)\n'
  279. for i in range(start, end + 1):
  280. expected += f'{i}|Line {i}\n'
  281. if end == total_lines:
  282. expected += '(this is the end of the file)\n'
  283. else:
  284. expected += f'({total_lines - end} more lines below)\n'
  285. assert result.split('\n') == expected.split('\n')
  286. def test_scroll_down_edge(tmp_path):
  287. temp_file_path = tmp_path / 'a.txt'
  288. content = '\n'.join([f'Line {i}' for i in range(1, 10)])
  289. temp_file_path.write_text(content)
  290. with io.StringIO() as buf:
  291. with contextlib.redirect_stdout(buf):
  292. open_file(str(temp_file_path))
  293. result = buf.getvalue()
  294. assert result is not None
  295. expected = f'[File: {temp_file_path} (9 lines total)]\n'
  296. expected += '(this is the beginning of the file)\n'
  297. for i in range(1, 10):
  298. expected += f'{i}|Line {i}\n'
  299. expected += '(this is the end of the file)\n'
  300. with io.StringIO() as buf:
  301. with contextlib.redirect_stdout(buf):
  302. scroll_down()
  303. result = buf.getvalue()
  304. assert result is not None
  305. # expected should be unchanged
  306. assert result.split('\n') == expected.split('\n')
  307. def test_print_window_internal(tmp_path):
  308. test_file_path = tmp_path / 'a.txt'
  309. test_file_path.write_text('')
  310. open_file(str(test_file_path))
  311. with open(test_file_path, 'w') as file:
  312. for i in range(1, 101):
  313. file.write(f'Line `{i}`\n')
  314. # Define the parameters for the test
  315. current_line = 50
  316. window = 2
  317. # Test _print_window especially with backticks
  318. with io.StringIO() as buf:
  319. with contextlib.redirect_stdout(buf):
  320. _print_window(str(test_file_path), current_line, window, return_str=False)
  321. result = buf.getvalue()
  322. expected = (
  323. '(48 more lines above)\n'
  324. '49|Line `49`\n'
  325. '50|Line `50`\n'
  326. '51|Line `51`\n'
  327. '(49 more lines below)\n'
  328. )
  329. assert result == expected
  330. def test_open_file_large_line_number(tmp_path):
  331. test_file_path = tmp_path / 'a.txt'
  332. test_file_path.write_text('')
  333. open_file(str(test_file_path))
  334. with open(test_file_path, 'w') as file:
  335. for i in range(1, 1000):
  336. file.write(f'Line `{i}`\n')
  337. # Define the parameters for the test
  338. current_line = 800
  339. window = 100
  340. # Test _print_window especially with backticks
  341. with io.StringIO() as buf:
  342. with contextlib.redirect_stdout(buf):
  343. # _print_window(str(test_file_path), current_line, window, return_str=False)
  344. open_file(str(test_file_path), current_line, window)
  345. result = buf.getvalue()
  346. expected = f'[File: {test_file_path} (999 lines total)]\n'
  347. expected += '(749 more lines above)\n'
  348. for i in range(750, 850 + 1):
  349. expected += f'{i}|Line `{i}`\n'
  350. expected += '(149 more lines below)\n'
  351. expected += '[Use `scroll_down` to view the next 100 lines of the file!]\n'
  352. assert result == expected
  353. def test_search_dir(tmp_path):
  354. # create files with the search term "bingo"
  355. for i in range(1, 101):
  356. temp_file_path = tmp_path / f'a{i}.txt'
  357. with open(temp_file_path, 'w') as file:
  358. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  359. if i == 50:
  360. file.write('bingo')
  361. # test
  362. with io.StringIO() as buf:
  363. with contextlib.redirect_stdout(buf):
  364. search_dir('bingo', str(tmp_path))
  365. result = buf.getvalue()
  366. assert result is not None
  367. expected = (
  368. f'[Found 1 matches for "bingo" in {tmp_path}]\n'
  369. f'{tmp_path}/a50.txt (Line 6): bingo\n'
  370. f'[End of matches for "bingo" in {tmp_path}]\n'
  371. )
  372. assert result.split('\n') == expected.split('\n')
  373. def test_search_dir_not_exist_term(tmp_path):
  374. # create files with the search term "bingo"
  375. for i in range(1, 101):
  376. temp_file_path = tmp_path / f'a{i}.txt'
  377. with open(temp_file_path, 'w') as file:
  378. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  379. # test
  380. with io.StringIO() as buf:
  381. with contextlib.redirect_stdout(buf):
  382. search_dir('non-exist', str(tmp_path))
  383. result = buf.getvalue()
  384. assert result is not None
  385. expected = f'No matches found for "non-exist" in {tmp_path}\n'
  386. assert result.split('\n') == expected.split('\n')
  387. def test_search_dir_too_much_match(tmp_path):
  388. # create files with the search term "Line 5"
  389. for i in range(1, 1000):
  390. temp_file_path = tmp_path / f'a{i}.txt'
  391. with open(temp_file_path, 'w') as file:
  392. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  393. with io.StringIO() as buf:
  394. with contextlib.redirect_stdout(buf):
  395. search_dir('Line 5', str(tmp_path))
  396. result = buf.getvalue()
  397. assert result is not None
  398. expected = f'More than 999 files matched for "Line 5" in {tmp_path}. Please narrow your search.\n'
  399. assert result.split('\n') == expected.split('\n')
  400. def test_search_dir_cwd(tmp_path, monkeypatch):
  401. # Using pytest's monkeypatch to change directory without affecting other tests
  402. monkeypatch.chdir(tmp_path)
  403. # create files with the search term "bingo"
  404. for i in range(1, 101):
  405. temp_file_path = tmp_path / f'a{i}.txt'
  406. with open(temp_file_path, 'w') as file:
  407. file.write('Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n')
  408. if i == 50:
  409. file.write('bingo')
  410. with io.StringIO() as buf:
  411. with contextlib.redirect_stdout(buf):
  412. search_dir('bingo')
  413. result = buf.getvalue()
  414. assert result is not None
  415. expected = (
  416. '[Found 1 matches for "bingo" in ./]\n'
  417. './a50.txt (Line 6): bingo\n'
  418. '[End of matches for "bingo" in ./]\n'
  419. )
  420. assert result.split('\n') == expected.split('\n')
  421. def test_search_file(tmp_path):
  422. temp_file_path = tmp_path / 'a.txt'
  423. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  424. with io.StringIO() as buf:
  425. with contextlib.redirect_stdout(buf):
  426. search_file('Line 5', str(temp_file_path))
  427. result = buf.getvalue()
  428. assert result is not None
  429. expected = f'[Found 1 matches for "Line 5" in {temp_file_path}]\n'
  430. expected += 'Line 5: Line 5\n'
  431. expected += f'[End of matches for "Line 5" in {temp_file_path}]\n'
  432. assert result.split('\n') == expected.split('\n')
  433. def test_search_file_not_exist_term(tmp_path):
  434. temp_file_path = tmp_path / 'a.txt'
  435. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  436. with io.StringIO() as buf:
  437. with contextlib.redirect_stdout(buf):
  438. search_file('Line 6', str(temp_file_path))
  439. result = buf.getvalue()
  440. assert result is not None
  441. expected = f'[No matches found for "Line 6" in {temp_file_path}]\n'
  442. assert result.split('\n') == expected.split('\n')
  443. def test_search_file_not_exist_file():
  444. _capture_file_operation_error(
  445. lambda: search_file('Line 6', '/unexist/path/a.txt'),
  446. 'ERROR: File /unexist/path/a.txt not found.',
  447. )
  448. def test_find_file(tmp_path):
  449. temp_file_path = tmp_path / 'a.txt'
  450. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  451. with io.StringIO() as buf:
  452. with contextlib.redirect_stdout(buf):
  453. find_file('a.txt', str(tmp_path))
  454. result = buf.getvalue()
  455. assert result is not None
  456. expected = f'[Found 1 matches for "a.txt" in {tmp_path}]\n'
  457. expected += f'{tmp_path}/a.txt\n'
  458. expected += f'[End of matches for "a.txt" in {tmp_path}]\n'
  459. assert result.split('\n') == expected.split('\n')
  460. def test_find_file_cwd(tmp_path, monkeypatch):
  461. monkeypatch.chdir(tmp_path)
  462. temp_file_path = tmp_path / 'a.txt'
  463. temp_file_path.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  464. with io.StringIO() as buf:
  465. with contextlib.redirect_stdout(buf):
  466. find_file('a.txt')
  467. result = buf.getvalue()
  468. assert result is not None
  469. def test_find_file_not_exist_file():
  470. with io.StringIO() as buf:
  471. with contextlib.redirect_stdout(buf):
  472. find_file('nonexist.txt')
  473. result = buf.getvalue()
  474. assert result is not None
  475. expected = '[No matches found for "nonexist.txt" in ./]\n'
  476. assert result.split('\n') == expected.split('\n')
  477. def test_find_file_not_exist_file_specific_path(tmp_path):
  478. with io.StringIO() as buf:
  479. with contextlib.redirect_stdout(buf):
  480. find_file('nonexist.txt', str(tmp_path))
  481. result = buf.getvalue()
  482. assert result is not None
  483. expected = f'[No matches found for "nonexist.txt" in {tmp_path}]\n'
  484. assert result.split('\n') == expected.split('\n')
  485. def test_parse_docx(tmp_path):
  486. # Create a DOCX file with some content
  487. test_docx_path = tmp_path / 'test.docx'
  488. doc = docx.Document()
  489. doc.add_paragraph('Hello, this is a test document.')
  490. doc.add_paragraph('This is the second paragraph.')
  491. doc.save(str(test_docx_path))
  492. old_stdout = sys.stdout
  493. sys.stdout = io.StringIO()
  494. # Call the parse_docx function
  495. parse_docx(str(test_docx_path))
  496. # Capture the output
  497. output = sys.stdout.getvalue()
  498. sys.stdout = old_stdout
  499. # Check if the output is correct
  500. expected_output = (
  501. f'[Reading DOCX file from {test_docx_path}]\n'
  502. '@@ Page 1 @@\nHello, this is a test document.\n\n'
  503. '@@ Page 2 @@\nThis is the second paragraph.\n\n\n'
  504. )
  505. assert output == expected_output, f'Expected output does not match. Got: {output}'
  506. def test_parse_latex(tmp_path):
  507. # Create a LaTeX file with some content
  508. test_latex_path = tmp_path / 'test.tex'
  509. with open(test_latex_path, 'w') as f:
  510. f.write(r"""
  511. \documentclass{article}
  512. \begin{document}
  513. Hello, this is a test LaTeX document.
  514. \end{document}
  515. """)
  516. old_stdout = sys.stdout
  517. sys.stdout = io.StringIO()
  518. # Call the parse_latex function
  519. parse_latex(str(test_latex_path))
  520. # Capture the output
  521. output = sys.stdout.getvalue()
  522. sys.stdout = old_stdout
  523. # Check if the output is correct
  524. expected_output = (
  525. f'[Reading LaTex file from {test_latex_path}]\n'
  526. 'Hello, this is a test LaTeX document.\n'
  527. )
  528. assert output == expected_output, f'Expected output does not match. Got: {output}'
  529. def test_parse_pdf(tmp_path):
  530. # Create a PDF file with some content
  531. test_pdf_path = tmp_path / 'test.pdf'
  532. from reportlab.lib.pagesizes import letter
  533. from reportlab.pdfgen import canvas
  534. c = canvas.Canvas(str(test_pdf_path), pagesize=letter)
  535. c.drawString(100, 750, 'Hello, this is a test PDF document.')
  536. c.save()
  537. old_stdout = sys.stdout
  538. sys.stdout = io.StringIO()
  539. # Call the parse_pdf function
  540. parse_pdf(str(test_pdf_path))
  541. # Capture the output
  542. output = sys.stdout.getvalue()
  543. sys.stdout = old_stdout
  544. # Check if the output is correct
  545. expected_output = (
  546. f'[Reading PDF file from {test_pdf_path}]\n'
  547. '@@ Page 1 @@\n'
  548. 'Hello, this is a test PDF document.\n'
  549. )
  550. assert output == expected_output, f'Expected output does not match. Got: {output}'
  551. def test_parse_pptx(tmp_path):
  552. test_pptx_path = tmp_path / 'test.pptx'
  553. from pptx import Presentation
  554. pres = Presentation()
  555. slide1 = pres.slides.add_slide(pres.slide_layouts[0])
  556. title1 = slide1.shapes.title
  557. title1.text = 'Hello, this is the first test PPTX slide.'
  558. slide2 = pres.slides.add_slide(pres.slide_layouts[0])
  559. title2 = slide2.shapes.title
  560. title2.text = 'Hello, this is the second test PPTX slide.'
  561. pres.save(str(test_pptx_path))
  562. old_stdout = sys.stdout
  563. sys.stdout = io.StringIO()
  564. parse_pptx(str(test_pptx_path))
  565. output = sys.stdout.getvalue()
  566. sys.stdout = old_stdout
  567. expected_output = (
  568. f'[Reading PowerPoint file from {test_pptx_path}]\n'
  569. '@@ Slide 1 @@\n'
  570. 'Hello, this is the first test PPTX slide.\n\n'
  571. '@@ Slide 2 @@\n'
  572. 'Hello, this is the second test PPTX slide.\n\n'
  573. )
  574. assert output == expected_output, f'Expected output does not match. Got: {output}'
  575. # =============================================================================
  576. def test_file_editor_view(tmp_path):
  577. # generate a random directory
  578. random_dir = tmp_path / 'dir_1'
  579. random_dir.mkdir()
  580. # create a file in the directory
  581. random_file = random_dir / 'a.txt'
  582. random_file.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  583. random_dir_2 = tmp_path / 'dir_2'
  584. random_dir_2.mkdir()
  585. random_file_2 = random_dir_2 / 'b.txt'
  586. random_file_2.write_text('Line 1\nLine 2\nLine 3\nLine 4\nLine 5')
  587. from openhands.runtime.plugins.agent_skills.agentskills import file_editor
  588. # view the file
  589. result = file_editor(command='view', path=str(random_file))
  590. print('\n', result)
  591. assert result is not None
  592. assert (
  593. result.split('\n')
  594. == f"""Here's the result of running `cat -n` on {random_file}:
  595. 1\tLine 1
  596. 2\tLine 2
  597. 3\tLine 3
  598. 4\tLine 4
  599. 5\tLine 5
  600. """.split('\n')
  601. )
  602. # view the directory
  603. result = file_editor(command='view', path=str(tmp_path))
  604. print('\n', result)
  605. assert result is not None
  606. assert (
  607. result.strip().split('\n')
  608. == f"""Here's the files and directories up to 2 levels deep in {tmp_path}, excluding hidden items:
  609. {tmp_path}
  610. {tmp_path}/dir_2
  611. {tmp_path}/dir_2/b.txt
  612. {tmp_path}/dir_1
  613. {tmp_path}/dir_1/a.txt
  614. """.strip().split('\n')
  615. )
  616. def test_file_editor_create(tmp_path):
  617. # generate a random directory
  618. random_dir = tmp_path / 'dir_1'
  619. random_dir.mkdir()
  620. # create a file in the directory
  621. random_file = random_dir / 'a.txt'
  622. from openhands.runtime.plugins.agent_skills.agentskills import file_editor
  623. # view an unexist file
  624. result = file_editor(command='view', path=str(random_file))
  625. print(result)
  626. assert result is not None
  627. assert (
  628. result
  629. == f'ERROR:\nThe path {random_file} does not exist. Please provide a valid path.'
  630. )
  631. # create a file
  632. result = file_editor(command='create', path=str(random_file), file_text='Line 6')
  633. print(result)
  634. assert result is not None
  635. assert result == f'File created successfully at: {random_file}'
  636. # view again
  637. result = file_editor(command='view', path=str(random_file))
  638. print(result)
  639. assert result is not None
  640. assert (
  641. result.strip().split('\n')
  642. == f"""Here's the result of running `cat -n` on {random_file}:
  643. 1\tLine 6
  644. """.strip().split('\n')
  645. )
  646. @pytest.fixture
  647. def setup_file(tmp_path):
  648. random_dir = tmp_path / 'dir_1'
  649. random_dir.mkdir()
  650. random_file = random_dir / 'a.txt'
  651. return random_file
  652. def test_file_editor_create_and_view(setup_file):
  653. random_file = setup_file
  654. # Test create command
  655. result = file_editor(
  656. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  657. )
  658. print(result)
  659. assert result == f'File created successfully at: {random_file}'
  660. # Test view command for file
  661. result = file_editor(command='view', path=str(random_file))
  662. print(result)
  663. assert (
  664. result.strip().split('\n')
  665. == f"""Here's the result of running `cat -n` on {random_file}:
  666. 1\tLine 1
  667. 2\tLine 2
  668. 3\tLine 3
  669. """.strip().split('\n')
  670. )
  671. # Test view command for directory
  672. result = file_editor(command='view', path=str(random_file.parent))
  673. assert f'{random_file.parent}' in result
  674. assert f'{random_file.name}' in result
  675. def test_file_editor_view_nonexistent(setup_file):
  676. random_file = setup_file
  677. # Test view command for non-existent file
  678. result = file_editor(command='view', path=str(random_file))
  679. assert (
  680. result
  681. == f'ERROR:\nThe path {random_file} does not exist. Please provide a valid path.'
  682. )
  683. def test_file_editor_str_replace(setup_file):
  684. random_file = setup_file
  685. file_editor(
  686. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  687. )
  688. # Test str_replace command
  689. result = file_editor(
  690. command='str_replace',
  691. path=str(random_file),
  692. old_str='Line 2',
  693. new_str='New Line 2',
  694. )
  695. print(result)
  696. assert (
  697. result
  698. == f"""The file {random_file} has been edited. Here's the result of running `cat -n` on a snippet of {random_file}:
  699. 1\tLine 1
  700. 2\tNew Line 2
  701. 3\tLine 3
  702. Review the changes and make sure they are as expected. Edit the file again if necessary."""
  703. )
  704. # View the file after str_replace
  705. result = file_editor(command='view', path=str(random_file))
  706. print(result)
  707. assert (
  708. result.strip().split('\n')
  709. == f"""Here's the result of running `cat -n` on {random_file}:
  710. 1\tLine 1
  711. 2\tNew Line 2
  712. 3\tLine 3
  713. """.strip().split('\n')
  714. )
  715. def test_file_editor_str_replace_non_existent(setup_file):
  716. random_file = setup_file
  717. file_editor(
  718. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  719. )
  720. # Test str_replace with non-existent string
  721. result = file_editor(
  722. command='str_replace',
  723. path=str(random_file),
  724. old_str='Non-existent Line',
  725. new_str='New Line',
  726. )
  727. print(result)
  728. assert (
  729. result
  730. == f'ERROR:\nNo replacement was performed, old_str `Non-existent Line` did not appear verbatim in {random_file}.'
  731. )
  732. def test_file_editor_insert(setup_file):
  733. random_file = setup_file
  734. file_editor(
  735. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  736. )
  737. # Test insert command
  738. result = file_editor(
  739. command='insert', path=str(random_file), insert_line=2, new_str='Inserted Line'
  740. )
  741. print(result)
  742. assert (
  743. result
  744. == f"""The file {random_file} has been edited. Here's the result of running `cat -n` on a snippet of the edited file:
  745. 1\tLine 1
  746. 2\tLine 2
  747. 3\tInserted Line
  748. 4\tLine 3
  749. Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."""
  750. )
  751. # View the file after insert
  752. result = file_editor(command='view', path=str(random_file))
  753. assert (
  754. result.strip().split('\n')
  755. == f"""Here's the result of running `cat -n` on {random_file}:
  756. 1\tLine 1
  757. 2\tLine 2
  758. 3\tInserted Line
  759. 4\tLine 3
  760. """.strip().split('\n')
  761. )
  762. def test_file_editor_insert_invalid_line(setup_file):
  763. random_file = setup_file
  764. file_editor(
  765. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  766. )
  767. # Test insert with invalid line number
  768. result = file_editor(
  769. command='insert',
  770. path=str(random_file),
  771. insert_line=10,
  772. new_str='Invalid Insert',
  773. )
  774. assert (
  775. result
  776. == 'ERROR:\nInvalid `insert_line` parameter: 10. It should be within the range of lines of the file: [0, 3]'
  777. )
  778. def test_file_editor_undo_edit(setup_file):
  779. random_file = setup_file
  780. result = file_editor(
  781. command='create', path=str(random_file), file_text='Line 1\nLine 2\nLine 3'
  782. )
  783. print(result)
  784. assert result == f"""File created successfully at: {random_file}"""
  785. # Make an edit
  786. result = file_editor(
  787. command='str_replace',
  788. path=str(random_file),
  789. old_str='Line 2',
  790. new_str='New Line 2',
  791. )
  792. print(result)
  793. assert (
  794. result
  795. == f"""The file {random_file} has been edited. Here's the result of running `cat -n` on a snippet of {random_file}:
  796. 1\tLine 1
  797. 2\tNew Line 2
  798. 3\tLine 3
  799. Review the changes and make sure they are as expected. Edit the file again if necessary."""
  800. )
  801. # Test undo_edit command
  802. result = file_editor(command='undo_edit', path=str(random_file))
  803. print(result)
  804. assert (
  805. result
  806. == f"""Last edit to {random_file} undone successfully. Here's the result of running `cat -n` on {random_file}:
  807. 1\tLine 1
  808. 2\tLine 2
  809. 3\tLine 3
  810. """
  811. )
  812. # View the file after undo_edit
  813. result = file_editor(command='view', path=str(random_file))
  814. assert (
  815. result.strip().split('\n')
  816. == f"""Here's the result of running `cat -n` on {random_file}:
  817. 1\tLine 1
  818. 2\tLine 2
  819. 3\tLine 3
  820. """.strip().split('\n')
  821. )
  822. def test_file_editor_undo_edit_no_edits(tmp_path):
  823. random_file = tmp_path / 'a.txt'
  824. random_file.touch()
  825. # Test undo_edit when no edits have been made
  826. result = file_editor(command='undo_edit', path=str(random_file))
  827. print(result)
  828. assert result == f'ERROR:\nNo edit history found for {random_file}.'