test_ipython.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. """Test the EventStreamRuntime, which connects to the RuntimeClient running in the sandbox."""
  2. import pytest
  3. from conftest import (
  4. TEST_IN_CI,
  5. _close_test_runtime,
  6. _get_sandbox_folder,
  7. _load_runtime,
  8. )
  9. from openhands.core.logger import openhands_logger as logger
  10. from openhands.events.action import (
  11. CmdRunAction,
  12. FileReadAction,
  13. FileWriteAction,
  14. IPythonRunCellAction,
  15. )
  16. from openhands.events.observation import (
  17. CmdOutputObservation,
  18. ErrorObservation,
  19. FileReadObservation,
  20. FileWriteObservation,
  21. IPythonRunCellObservation,
  22. )
  23. # ============================================================================================================================
  24. # ipython-specific tests
  25. # ============================================================================================================================
  26. def test_simple_cmd_ipython_and_fileop(temp_dir, box_class, run_as_openhands):
  27. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  28. sandbox_dir = _get_sandbox_folder(runtime)
  29. # Test run command
  30. action_cmd = CmdRunAction(command='ls -l')
  31. logger.info(action_cmd, extra={'msg_type': 'ACTION'})
  32. obs = runtime.run_action(action_cmd)
  33. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  34. assert isinstance(obs, CmdOutputObservation)
  35. assert obs.exit_code == 0
  36. assert 'total 0' in obs.content
  37. # Test run ipython
  38. test_code = "print('Hello, `World`!\\n')"
  39. action_ipython = IPythonRunCellAction(code=test_code)
  40. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  41. obs = runtime.run_action(action_ipython)
  42. assert isinstance(obs, IPythonRunCellObservation)
  43. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  44. assert obs.content.strip() == (
  45. 'Hello, `World`!\n'
  46. f'[Jupyter current working directory: {sandbox_dir}]\n'
  47. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  48. )
  49. # Test read file (file should not exist)
  50. action_read = FileReadAction(path='hello.sh')
  51. logger.info(action_read, extra={'msg_type': 'ACTION'})
  52. obs = runtime.run_action(action_read)
  53. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  54. assert isinstance(obs, ErrorObservation)
  55. assert 'File not found' in obs.content
  56. # Test write file
  57. action_write = FileWriteAction(content='echo "Hello, World!"', path='hello.sh')
  58. logger.info(action_write, extra={'msg_type': 'ACTION'})
  59. obs = runtime.run_action(action_write)
  60. assert isinstance(obs, FileWriteObservation)
  61. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  62. assert obs.content == ''
  63. # event stream runtime will always use absolute path
  64. assert obs.path == f'{sandbox_dir}/hello.sh'
  65. # Test read file (file should exist)
  66. action_read = FileReadAction(path='hello.sh')
  67. logger.info(action_read, extra={'msg_type': 'ACTION'})
  68. obs = runtime.run_action(action_read)
  69. assert isinstance(
  70. obs, FileReadObservation
  71. ), 'The observation should be a FileReadObservation.'
  72. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  73. assert obs.content == 'echo "Hello, World!"\n'
  74. assert obs.path == f'{sandbox_dir}/hello.sh'
  75. # clean up
  76. action = CmdRunAction(command='rm -rf hello.sh')
  77. logger.info(action, extra={'msg_type': 'ACTION'})
  78. obs = runtime.run_action(action)
  79. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  80. assert obs.exit_code == 0
  81. _close_test_runtime(runtime)
  82. @pytest.mark.skipif(
  83. TEST_IN_CI != 'True',
  84. reason='This test is not working in WSL (file ownership)',
  85. )
  86. def test_ipython_multi_user(temp_dir, box_class, run_as_openhands):
  87. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  88. # Test run ipython
  89. # get username
  90. test_code = "import os; print(os.environ['USER'])"
  91. action_ipython = IPythonRunCellAction(code=test_code)
  92. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  93. obs = runtime.run_action(action_ipython)
  94. assert isinstance(obs, IPythonRunCellObservation)
  95. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  96. if run_as_openhands:
  97. assert 'openhands' in obs.content
  98. else:
  99. assert 'root' in obs.content
  100. # print the current working directory
  101. test_code = 'import os; print(os.getcwd())'
  102. action_ipython = IPythonRunCellAction(code=test_code)
  103. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  104. obs = runtime.run_action(action_ipython)
  105. assert isinstance(obs, IPythonRunCellObservation)
  106. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  107. assert (
  108. obs.content.strip()
  109. == (
  110. '/workspace\n'
  111. '[Jupyter current working directory: /workspace]\n'
  112. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  113. ).strip()
  114. )
  115. # write a file
  116. test_code = "with open('test.txt', 'w') as f: f.write('Hello, world!')"
  117. action_ipython = IPythonRunCellAction(code=test_code)
  118. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  119. obs = runtime.run_action(action_ipython)
  120. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  121. assert isinstance(obs, IPythonRunCellObservation)
  122. assert (
  123. obs.content.strip()
  124. == (
  125. '[Code executed successfully with no output]\n'
  126. '[Jupyter current working directory: /workspace]\n'
  127. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  128. ).strip()
  129. )
  130. # check file owner via bash
  131. action = CmdRunAction(command='ls -alh test.txt')
  132. logger.info(action, extra={'msg_type': 'ACTION'})
  133. obs = runtime.run_action(action)
  134. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  135. assert obs.exit_code == 0
  136. if run_as_openhands:
  137. # -rw-r--r-- 1 openhands root 13 Jul 28 03:53 test.txt
  138. assert 'openhands' in obs.content.split('\r\n')[0]
  139. else:
  140. # -rw-r--r-- 1 root root 13 Jul 28 03:53 test.txt
  141. assert 'root' in obs.content.split('\r\n')[0]
  142. # clean up
  143. action = CmdRunAction(command='rm -rf test')
  144. logger.info(action, extra={'msg_type': 'ACTION'})
  145. obs = runtime.run_action(action)
  146. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  147. assert obs.exit_code == 0
  148. _close_test_runtime(runtime)
  149. def test_ipython_simple(temp_dir, box_class):
  150. runtime = _load_runtime(temp_dir, box_class)
  151. sandbox_dir = _get_sandbox_folder(runtime)
  152. # Test run ipython
  153. # get username
  154. test_code = 'print(1)'
  155. action_ipython = IPythonRunCellAction(code=test_code)
  156. logger.info(action_ipython, extra={'msg_type': 'ACTION'})
  157. obs = runtime.run_action(action_ipython)
  158. assert isinstance(obs, IPythonRunCellObservation)
  159. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  160. assert (
  161. obs.content.strip()
  162. == (
  163. '1\n'
  164. f'[Jupyter current working directory: {sandbox_dir}]\n'
  165. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  166. ).strip()
  167. )
  168. _close_test_runtime(runtime)
  169. def test_ipython_package_install(temp_dir, box_class, run_as_openhands):
  170. """Make sure that cd in bash also update the current working directory in ipython."""
  171. runtime = _load_runtime(temp_dir, box_class, run_as_openhands)
  172. sandbox_dir = _get_sandbox_folder(runtime)
  173. # It should error out since pymsgbox is not installed
  174. action = IPythonRunCellAction(code='import pymsgbox')
  175. logger.info(action, extra={'msg_type': 'ACTION'})
  176. obs = runtime.run_action(action)
  177. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  178. assert "ModuleNotFoundError: No module named 'pymsgbox'" in obs.content
  179. # Install pymsgbox in Jupyter
  180. action = IPythonRunCellAction(code='%pip install pymsgbox==1.0.9')
  181. logger.info(action, extra={'msg_type': 'ACTION'})
  182. obs = runtime.run_action(action)
  183. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  184. assert (
  185. 'Successfully installed pymsgbox-1.0.9' in obs.content
  186. or '[Package installed successfully]' in obs.content
  187. )
  188. action = IPythonRunCellAction(code='import pymsgbox')
  189. logger.info(action, extra={'msg_type': 'ACTION'})
  190. obs = runtime.run_action(action)
  191. logger.info(obs, extra={'msg_type': 'OBSERVATION'})
  192. # import should not error out
  193. assert obs.content.strip() == (
  194. '[Code executed successfully with no output]\n'
  195. f'[Jupyter current working directory: {sandbox_dir}]\n'
  196. '[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
  197. )
  198. _close_test_runtime(runtime)