test_sandbox.py 11 KB

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