test_sandbox.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import os
  2. import pathlib
  3. import tempfile
  4. import pytest
  5. from opendevin.core.config import AppConfig, SandboxConfig
  6. from opendevin.runtime.docker.ssh_box import DockerSSHBox
  7. from opendevin.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
  8. def create_docker_box_from_app_config(
  9. path: str, config: AppConfig | None = None
  10. ) -> DockerSSHBox:
  11. if config is None:
  12. config = AppConfig(
  13. sandbox=SandboxConfig(
  14. box_type='ssh',
  15. ),
  16. persist_sandbox=False,
  17. )
  18. return DockerSSHBox(
  19. config=config.sandbox,
  20. persist_sandbox=config.persist_sandbox,
  21. workspace_mount_path=path,
  22. sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
  23. cache_dir=config.cache_dir,
  24. run_as_devin=True,
  25. ssh_hostname=config.ssh_hostname,
  26. ssh_password=config.ssh_password,
  27. ssh_port=config.ssh_port,
  28. )
  29. @pytest.fixture
  30. def temp_dir(monkeypatch):
  31. # get a temporary directory
  32. with tempfile.TemporaryDirectory() as temp_dir:
  33. pathlib.Path().mkdir(parents=True, exist_ok=True)
  34. yield temp_dir
  35. def test_ssh_box_run_as_devin(temp_dir):
  36. # get a temporary directory
  37. for box in [
  38. create_docker_box_from_app_config(temp_dir),
  39. ]: # FIXME: permission error on mkdir test for exec box
  40. exit_code, output = box.execute('ls -l')
  41. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  42. assert output.strip() == 'total 0'
  43. assert box.workspace_mount_path == temp_dir
  44. exit_code, output = box.execute('ls -l')
  45. assert exit_code == 0, 'The exit code should be 0.'
  46. assert output.strip() == 'total 0'
  47. exit_code, output = box.execute('mkdir test')
  48. assert exit_code == 0, 'The exit code should be 0.'
  49. assert output.strip() == ''
  50. exit_code, output = box.execute('ls -l')
  51. assert exit_code == 0, 'The exit code should be 0.'
  52. assert 'opendevin' in output, "The output should contain username 'opendevin'"
  53. assert 'test' in output, 'The output should contain the test directory'
  54. exit_code, output = box.execute('touch test/foo.txt')
  55. assert exit_code == 0, 'The exit code should be 0.'
  56. assert output.strip() == ''
  57. exit_code, output = box.execute('ls -l test')
  58. assert exit_code == 0, 'The exit code should be 0.'
  59. assert 'foo.txt' in output, 'The output should contain the foo.txt file'
  60. box.close()
  61. def test_ssh_box_multi_line_cmd_run_as_devin(temp_dir):
  62. box = create_docker_box_from_app_config(temp_dir)
  63. exit_code, output = box.execute('pwd && ls -l')
  64. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  65. expected_lines = ['/workspace', 'total 0']
  66. line_sep = '\r\n' if isinstance(box, DockerSSHBox) else '\n'
  67. assert output == line_sep.join(expected_lines), (
  68. 'The output should be the same as the input for ' + box.__class__.__name__
  69. )
  70. box.close()
  71. def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
  72. box = create_docker_box_from_app_config(temp_dir)
  73. exit_code, output = box.execute('mkdir test')
  74. assert exit_code == 0, 'The exit code should be 0.'
  75. assert output.strip() == ''
  76. exit_code, output = box.execute('cd test')
  77. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  78. assert output.strip() == '', (
  79. 'The output should be empty for ' + box.__class__.__name__
  80. )
  81. exit_code, output = box.execute('pwd')
  82. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  83. assert output.strip() == '/workspace/test', (
  84. 'The output should be /workspace for ' + box.__class__.__name__
  85. )
  86. box.close()
  87. def test_ssh_box_failed_cmd_run_as_devin(temp_dir):
  88. box = create_docker_box_from_app_config(temp_dir)
  89. exit_code, output = box.execute('non_existing_command')
  90. assert exit_code != 0, (
  91. 'The exit code should not be 0 for a failed command for '
  92. + box.__class__.__name__
  93. )
  94. box.close()
  95. def test_single_multiline_command(temp_dir):
  96. box = create_docker_box_from_app_config(temp_dir)
  97. exit_code, output = box.execute('echo \\\n -e "foo"')
  98. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  99. # FIXME: why is there a `>` in the output? Probably PS2?
  100. assert output == '> foo', (
  101. 'The output should be the same as the input for ' + box.__class__.__name__
  102. )
  103. box.close()
  104. def test_multiline_echo(temp_dir):
  105. box = create_docker_box_from_app_config(temp_dir)
  106. exit_code, output = box.execute('echo -e "hello\nworld"')
  107. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  108. # FIXME: why is there a `>` in the output?
  109. assert output == '> hello\r\nworld', (
  110. 'The output should be the same as the input for ' + box.__class__.__name__
  111. )
  112. box.close()
  113. def test_sandbox_whitespace(temp_dir):
  114. box = create_docker_box_from_app_config(temp_dir)
  115. exit_code, output = box.execute('echo -e "\\n\\n\\n"')
  116. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  117. assert output == '\r\n\r\n\r\n', (
  118. 'The output should be the same as the input for ' + box.__class__.__name__
  119. )
  120. box.close()
  121. def test_sandbox_jupyter_plugin(temp_dir):
  122. box = create_docker_box_from_app_config(temp_dir)
  123. box.init_plugins([JupyterRequirement])
  124. exit_code, output = box.execute('echo "print(1)" | execute_cli')
  125. print(output)
  126. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  127. assert output == '1\r\n', (
  128. 'The output should be the same as the input for ' + box.__class__.__name__
  129. )
  130. box.close()
  131. def _test_sandbox_jupyter_agentskills_fileop_pwd_impl(box, config: AppConfig):
  132. box.init_plugins([AgentSkillsRequirement, JupyterRequirement])
  133. exit_code, output = box.execute('mkdir test')
  134. print(output)
  135. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  136. exit_code, output = box.execute('echo "create_file(\'hello.py\')" | execute_cli')
  137. print(output)
  138. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  139. assert output.strip().split('\r\n') == (
  140. '[File: /workspace/hello.py (1 lines total)]\r\n'
  141. '(this is the beginning of the file)\r\n'
  142. '1|\r\n'
  143. '(this is the end of the file)\r\n'
  144. '[File hello.py created.]\r\n'
  145. ).strip().split('\r\n')
  146. exit_code, output = box.execute('cd test')
  147. print(output)
  148. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  149. exit_code, output = box.execute('echo "create_file(\'hello.py\')" | execute_cli')
  150. print(output)
  151. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  152. assert output.strip().split('\r\n') == (
  153. '[File: /workspace/test/hello.py (1 lines total)]\r\n'
  154. '(this is the beginning of the file)\r\n'
  155. '1|\r\n'
  156. '(this is the end of the file)\r\n'
  157. '[File hello.py created.]\r\n'
  158. ).strip().split('\r\n')
  159. if config.sandbox.enable_auto_lint:
  160. # edit file, but make a mistake in indentation
  161. exit_code, output = box.execute(
  162. 'echo "insert_content_at_line(\'hello.py\', 1, \' print(\\"hello world\\")\')" | execute_cli'
  163. )
  164. print(output)
  165. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  166. assert output.strip().split('\r\n') == (
  167. """
  168. [Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
  169. ERRORS:
  170. /workspace/test/hello.py:1:3: E999 IndentationError: unexpected indent
  171. [This is how your edit would have looked if applied]
  172. -------------------------------------------------
  173. (this is the beginning of the file)
  174. 1| print("hello world")
  175. (this is the end of the file)
  176. -------------------------------------------------
  177. [This is the original code before your edit]
  178. -------------------------------------------------
  179. (this is the beginning of the file)
  180. 1|
  181. (this is the end of the file)
  182. -------------------------------------------------
  183. Your changes have NOT been applied. Please fix your edit command and try again.
  184. You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
  185. DO NOT re-run the same failed edit command. Running it again will lead to the same error.
  186. """
  187. ).strip().split('\n')
  188. # edit file with correct indentation
  189. exit_code, output = box.execute(
  190. 'echo "insert_content_at_line(\'hello.py\', 1, \'print(\\"hello world\\")\')" | execute_cli'
  191. )
  192. print(output)
  193. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  194. assert output.strip().split('\r\n') == (
  195. """
  196. [File: /workspace/test/hello.py (1 lines total after edit)]
  197. (this is the beginning of the file)
  198. 1|print("hello world")
  199. (this is the end of the file)
  200. [File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
  201. """
  202. ).strip().split('\n')
  203. exit_code, output = box.execute('rm -rf /workspace/*')
  204. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  205. box.close()
  206. def test_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
  207. # get a temporary directory
  208. config = AppConfig(
  209. sandbox=SandboxConfig(
  210. box_type='ssh',
  211. enable_auto_lint=False,
  212. ),
  213. persist_sandbox=False,
  214. )
  215. assert not config.sandbox.enable_auto_lint
  216. box = create_docker_box_from_app_config(temp_dir, config)
  217. _test_sandbox_jupyter_agentskills_fileop_pwd_impl(box, config)
  218. @pytest.mark.skipif(
  219. os.getenv('TEST_IN_CI') != 'true',
  220. reason='The unittest need to download image, so only run on CI',
  221. )
  222. def test_agnostic_sandbox_jupyter_agentskills_fileop_pwd(temp_dir):
  223. for base_sandbox_image in ['ubuntu:22.04', 'debian:11']:
  224. config = AppConfig(
  225. sandbox=SandboxConfig(
  226. box_type='ssh',
  227. container_image=base_sandbox_image,
  228. enable_auto_lint=False,
  229. ),
  230. persist_sandbox=False,
  231. )
  232. assert not config.sandbox.enable_auto_lint
  233. box = create_docker_box_from_app_config(temp_dir, config)
  234. _test_sandbox_jupyter_agentskills_fileop_pwd_impl(box, config)
  235. def test_sandbox_jupyter_plugin_backticks(temp_dir):
  236. config = AppConfig(
  237. sandbox=SandboxConfig(
  238. box_type='ssh',
  239. ),
  240. persist_sandbox=False,
  241. )
  242. box = DockerSSHBox(
  243. config=config.sandbox,
  244. persist_sandbox=config.persist_sandbox,
  245. workspace_mount_path=temp_dir,
  246. sandbox_workspace_dir=config.workspace_mount_path_in_sandbox,
  247. cache_dir=config.cache_dir,
  248. run_as_devin=True,
  249. ssh_hostname=config.ssh_hostname,
  250. ssh_password=config.ssh_password,
  251. ssh_port=config.ssh_port,
  252. )
  253. box.init_plugins([JupyterRequirement])
  254. test_code = "print('Hello, `World`!')"
  255. expected_write_command = (
  256. "cat > /tmp/opendevin_jupyter_temp.py <<'EOL'\n" f'{test_code}\n' 'EOL'
  257. )
  258. expected_execute_command = 'cat /tmp/opendevin_jupyter_temp.py | execute_cli'
  259. exit_code, output = box.execute(expected_write_command)
  260. exit_code, output = box.execute(expected_execute_command)
  261. print(output)
  262. assert exit_code == 0, 'The exit code should be 0 for ' + box.__class__.__name__
  263. assert output.strip() == 'Hello, `World`!', (
  264. 'The output should be the same as the input for ' + box.__class__.__name__
  265. )
  266. box.close()