test_sandbox.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import os
  2. import pathlib
  3. import tempfile
  4. from unittest.mock import patch
  5. import pytest
  6. from opendevin.core.config import config
  7. from opendevin.runtime.docker.exec_box import DockerExecBox
  8. from opendevin.runtime.docker.local_box import LocalBox
  9. from opendevin.runtime.docker.ssh_box import DockerSSHBox, split_bash_commands
  10. from opendevin.runtime.plugins import JupyterRequirement
  11. @pytest.fixture
  12. def temp_dir(monkeypatch):
  13. # get a temporary directory
  14. with tempfile.TemporaryDirectory() as temp_dir:
  15. pathlib.Path().mkdir(parents=True, exist_ok=True)
  16. yield temp_dir
  17. def test_env_vars(temp_dir):
  18. os.environ['SANDBOX_ENV_FOOBAR'] = 'BAZ'
  19. for box_class in [DockerSSHBox, DockerExecBox, LocalBox]:
  20. box = box_class()
  21. box.add_to_env('QUUX', 'abc"def')
  22. assert box._env['FOOBAR'] == 'BAZ'
  23. assert box._env['QUUX'] == 'abc"def'
  24. exit_code, output = box.execute('echo $FOOBAR $QUUX')
  25. assert exit_code == 0, 'The exit code should be 0.'
  26. assert output.strip() == 'BAZ abc"def', f'Output: {output} for {box_class}'
  27. def test_split_commands():
  28. cmds = [
  29. 'ls -l',
  30. 'echo -e "hello\nworld"',
  31. """
  32. echo -e 'hello it\\'s me'
  33. """.strip(),
  34. """
  35. echo \\
  36. -e 'hello' \\
  37. -v
  38. """.strip(),
  39. """
  40. echo -e 'hello\\nworld\\nare\\nyou\\nthere?'
  41. """.strip(),
  42. """
  43. echo -e 'hello
  44. world
  45. are
  46. you\\n
  47. there?'
  48. """.strip(),
  49. """
  50. echo -e 'hello
  51. world "
  52. '
  53. """.strip(),
  54. """
  55. kubectl apply -f - <<EOF
  56. apiVersion: v1
  57. kind: Pod
  58. metadata:
  59. name: busybox-sleep
  60. spec:
  61. containers:
  62. - name: busybox
  63. image: busybox:1.28
  64. args:
  65. - sleep
  66. - "1000000"
  67. EOF
  68. """.strip(),
  69. ]
  70. joined_cmds = '\n'.join(cmds)
  71. split_cmds = split_bash_commands(joined_cmds)
  72. for s in split_cmds:
  73. print('\nCMD')
  74. print(s)
  75. cmds = [
  76. c.replace('\\\n', '') for c in cmds
  77. ] # The function strips escaped newlines, but this shouldn't matter
  78. assert (
  79. split_cmds == cmds
  80. ), 'The split commands should be the same as the input commands.'
  81. def test_ssh_box_run_as_devin(temp_dir):
  82. # get a temporary directory
  83. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  84. config, 'workspace_mount_path', new=temp_dir
  85. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  86. config, 'sandbox_type', new='ssh'
  87. ):
  88. for box in [
  89. DockerSSHBox()
  90. ]: # FIXME: permission error on mkdir test for exec box
  91. exit_code, output = box.execute('ls -l')
  92. assert exit_code == 0, (
  93. 'The exit code should be 0 for ' + box.__class__.__name__
  94. )
  95. assert output.strip() == 'total 0'
  96. assert config.workspace_base == temp_dir
  97. exit_code, output = box.execute('ls -l')
  98. assert exit_code == 0, 'The exit code should be 0.'
  99. assert output.strip() == 'total 0'
  100. exit_code, output = box.execute('mkdir test')
  101. assert exit_code == 0, 'The exit code should be 0.'
  102. assert output.strip() == ''
  103. exit_code, output = box.execute('ls -l')
  104. assert exit_code == 0, 'The exit code should be 0.'
  105. assert (
  106. 'opendevin' in output
  107. ), "The output should contain username 'opendevin'"
  108. assert 'test' in output, 'The output should contain the test directory'
  109. exit_code, output = box.execute('touch test/foo.txt')
  110. assert exit_code == 0, 'The exit code should be 0.'
  111. assert output.strip() == ''
  112. exit_code, output = box.execute('ls -l test')
  113. assert exit_code == 0, 'The exit code should be 0.'
  114. assert 'foo.txt' in output, 'The output should contain the foo.txt file'
  115. box.close()
  116. def test_ssh_box_multi_line_cmd_run_as_devin(temp_dir):
  117. # get a temporary directory
  118. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  119. config, 'workspace_mount_path', new=temp_dir
  120. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  121. config, 'sandbox_type', new='ssh'
  122. ):
  123. for box in [DockerSSHBox(), DockerExecBox()]:
  124. exit_code, output = box.execute('pwd && ls -l')
  125. assert exit_code == 0, (
  126. 'The exit code should be 0 for ' + box.__class__.__name__
  127. )
  128. expected_lines = ['/workspace', 'total 0']
  129. line_sep = '\r\n' if isinstance(box, DockerSSHBox) else '\n'
  130. assert output == line_sep.join(expected_lines), (
  131. 'The output should be the same as the input for '
  132. + box.__class__.__name__
  133. )
  134. box.close()
  135. def test_ssh_box_stateful_cmd_run_as_devin(temp_dir):
  136. # get a temporary directory
  137. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  138. config, 'workspace_mount_path', new=temp_dir
  139. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  140. config, 'sandbox_type', new='ssh'
  141. ):
  142. for box in [
  143. DockerSSHBox()
  144. ]: # FIXME: DockerExecBox() does not work with stateful commands
  145. exit_code, output = box.execute('mkdir test')
  146. assert exit_code == 0, 'The exit code should be 0.'
  147. assert output.strip() == ''
  148. exit_code, output = box.execute('cd test')
  149. assert exit_code == 0, (
  150. 'The exit code should be 0 for ' + box.__class__.__name__
  151. )
  152. assert output.strip() == '', (
  153. 'The output should be empty for ' + box.__class__.__name__
  154. )
  155. exit_code, output = box.execute('pwd')
  156. assert exit_code == 0, (
  157. 'The exit code should be 0 for ' + box.__class__.__name__
  158. )
  159. assert output.strip() == '/workspace/test', (
  160. 'The output should be /workspace for ' + box.__class__.__name__
  161. )
  162. box.close()
  163. def test_ssh_box_failed_cmd_run_as_devin(temp_dir):
  164. # get a temporary directory
  165. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  166. config, 'workspace_mount_path', new=temp_dir
  167. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  168. config, 'sandbox_type', new='ssh'
  169. ):
  170. for box in [DockerSSHBox(), DockerExecBox()]:
  171. exit_code, output = box.execute('non_existing_command')
  172. assert exit_code != 0, (
  173. 'The exit code should not be 0 for a failed command for '
  174. + box.__class__.__name__
  175. )
  176. box.close()
  177. def test_single_multiline_command(temp_dir):
  178. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  179. config, 'workspace_mount_path', new=temp_dir
  180. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  181. config, 'sandbox_type', new='ssh'
  182. ):
  183. for box in [DockerSSHBox(), DockerExecBox()]:
  184. exit_code, output = box.execute('echo \\\n -e "foo"')
  185. assert exit_code == 0, (
  186. 'The exit code should be 0 for ' + box.__class__.__name__
  187. )
  188. if isinstance(box, DockerExecBox):
  189. assert output == 'foo', (
  190. 'The output should be the same as the input for '
  191. + box.__class__.__name__
  192. )
  193. else:
  194. # FIXME: why is there a `>` in the output? Probably PS2?
  195. assert output == '> foo', (
  196. 'The output should be the same as the input for '
  197. + box.__class__.__name__
  198. )
  199. box.close()
  200. def test_multiline_echo(temp_dir):
  201. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  202. config, 'workspace_mount_path', new=temp_dir
  203. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  204. config, 'sandbox_type', new='ssh'
  205. ):
  206. for box in [DockerSSHBox(), DockerExecBox()]:
  207. exit_code, output = box.execute('echo -e "hello\nworld"')
  208. assert exit_code == 0, (
  209. 'The exit code should be 0 for ' + box.__class__.__name__
  210. )
  211. if isinstance(box, DockerExecBox):
  212. assert output == 'hello\nworld', (
  213. 'The output should be the same as the input for '
  214. + box.__class__.__name__
  215. )
  216. else:
  217. # FIXME: why is there a `>` in the output?
  218. assert output == '> hello\r\nworld', (
  219. 'The output should be the same as the input for '
  220. + box.__class__.__name__
  221. )
  222. box.close()
  223. def test_sandbox_whitespace(temp_dir):
  224. # get a temporary directory
  225. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  226. config, 'workspace_mount_path', new=temp_dir
  227. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  228. config, 'sandbox_type', new='ssh'
  229. ):
  230. for box in [DockerSSHBox(), DockerExecBox()]:
  231. exit_code, output = box.execute('echo -e "\\n\\n\\n"')
  232. assert exit_code == 0, (
  233. 'The exit code should be 0 for ' + box.__class__.__name__
  234. )
  235. if isinstance(box, DockerExecBox):
  236. assert output == '\n\n\n', (
  237. 'The output should be the same as the input for '
  238. + box.__class__.__name__
  239. )
  240. else:
  241. assert output == '\r\n\r\n\r\n', (
  242. 'The output should be the same as the input for '
  243. + box.__class__.__name__
  244. )
  245. box.close()
  246. def test_sandbox_jupyter_plugin(temp_dir):
  247. # get a temporary directory
  248. with patch.object(config, 'workspace_base', new=temp_dir), patch.object(
  249. config, 'workspace_mount_path', new=temp_dir
  250. ), patch.object(config, 'run_as_devin', new='true'), patch.object(
  251. config, 'sandbox_type', new='ssh'
  252. ):
  253. for box in [DockerSSHBox()]:
  254. box.init_plugins([JupyterRequirement])
  255. exit_code, output = box.execute('echo "print(1)" | execute_cli')
  256. print(output)
  257. assert exit_code == 0, (
  258. 'The exit code should be 0 for ' + box.__class__.__name__
  259. )
  260. assert output == '1\r\n', (
  261. 'The output should be the same as the input for '
  262. + box.__class__.__name__
  263. )
  264. box.close()