test_runtime_build.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import os
  2. import tempfile
  3. from importlib.metadata import version
  4. from unittest.mock import ANY, MagicMock, call, patch
  5. import pytest
  6. import toml
  7. from pytest import TempPathFactory
  8. from opendevin.runtime.utils.runtime_build import (
  9. RUNTIME_IMAGE_REPO,
  10. _generate_dockerfile,
  11. _get_package_version,
  12. _put_source_code_to_dir,
  13. build_runtime_image,
  14. get_runtime_image_repo_and_tag,
  15. prep_docker_build_folder,
  16. )
  17. OD_VERSION = f'od_v{_get_package_version()}'
  18. @pytest.fixture
  19. def temp_dir(tmp_path_factory: TempPathFactory) -> str:
  20. return str(tmp_path_factory.mktemp('test_runtime_build'))
  21. def _check_source_code_in_dir(temp_dir):
  22. # assert there is a folder called 'code' in the temp_dir
  23. code_dir = os.path.join(temp_dir, 'code')
  24. assert os.path.exists(code_dir)
  25. assert os.path.isdir(code_dir)
  26. # check the source file is the same as the current code base
  27. assert os.path.exists(os.path.join(code_dir, 'pyproject.toml'))
  28. # The source code should only include the `opendevin` folder, but not the other folders
  29. assert set(os.listdir(code_dir)) == {
  30. 'opendevin',
  31. 'pyproject.toml',
  32. 'poetry.lock',
  33. 'LICENSE',
  34. 'README.md',
  35. 'PKG-INFO',
  36. }
  37. assert os.path.exists(os.path.join(code_dir, 'opendevin'))
  38. assert os.path.isdir(os.path.join(code_dir, 'opendevin'))
  39. # make sure the version from the pyproject.toml is the same as the current version
  40. with open(os.path.join(code_dir, 'pyproject.toml'), 'r') as f:
  41. pyproject = toml.load(f)
  42. _pyproject_version = pyproject['tool']['poetry']['version']
  43. assert _pyproject_version == version('opendevin')
  44. def test_put_source_code_to_dir(temp_dir):
  45. _put_source_code_to_dir(temp_dir)
  46. _check_source_code_in_dir(temp_dir)
  47. def test_docker_build_folder(temp_dir):
  48. prep_docker_build_folder(
  49. temp_dir,
  50. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  51. skip_init=False,
  52. )
  53. # check the source code is in the folder
  54. _check_source_code_in_dir(temp_dir)
  55. # Now check dockerfile is in the folder
  56. dockerfile_path = os.path.join(temp_dir, 'Dockerfile')
  57. assert os.path.exists(dockerfile_path)
  58. assert os.path.isfile(dockerfile_path)
  59. # check the folder only contains the source code and the Dockerfile
  60. assert set(os.listdir(temp_dir)) == {'code', 'Dockerfile'}
  61. def test_hash_folder_same(temp_dir):
  62. dir_hash_1 = prep_docker_build_folder(
  63. temp_dir,
  64. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  65. skip_init=False,
  66. )
  67. with tempfile.TemporaryDirectory() as temp_dir_2:
  68. dir_hash_2 = prep_docker_build_folder(
  69. temp_dir_2,
  70. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  71. skip_init=False,
  72. )
  73. assert dir_hash_1 == dir_hash_2
  74. def test_hash_folder_diff_init(temp_dir):
  75. dir_hash_1 = prep_docker_build_folder(
  76. temp_dir,
  77. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  78. skip_init=False,
  79. )
  80. with tempfile.TemporaryDirectory() as temp_dir_2:
  81. dir_hash_2 = prep_docker_build_folder(
  82. temp_dir_2,
  83. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  84. skip_init=True,
  85. )
  86. assert dir_hash_1 != dir_hash_2
  87. def test_hash_folder_diff_image(temp_dir):
  88. dir_hash_1 = prep_docker_build_folder(
  89. temp_dir,
  90. base_image='nikolaik/python-nodejs:python3.11-nodejs22',
  91. skip_init=False,
  92. )
  93. with tempfile.TemporaryDirectory() as temp_dir_2:
  94. dir_hash_2 = prep_docker_build_folder(
  95. temp_dir_2,
  96. base_image='debian:11',
  97. skip_init=False,
  98. )
  99. assert dir_hash_1 != dir_hash_2
  100. def test_generate_dockerfile_scratch():
  101. base_image = 'debian:11'
  102. dockerfile_content = _generate_dockerfile(
  103. base_image,
  104. skip_init=False,
  105. )
  106. assert base_image in dockerfile_content
  107. assert 'apt-get update' in dockerfile_content
  108. assert 'apt-get install -y wget sudo apt-utils' in dockerfile_content
  109. assert (
  110. 'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry python=3.11 -y'
  111. in dockerfile_content
  112. )
  113. # Check the update command
  114. assert 'COPY ./code /opendevin/code' in dockerfile_content
  115. assert (
  116. '/opendevin/miniforge3/bin/mamba run -n base poetry install'
  117. in dockerfile_content
  118. )
  119. def test_generate_dockerfile_skip_init():
  120. base_image = 'debian:11'
  121. dockerfile_content = _generate_dockerfile(
  122. base_image,
  123. skip_init=True,
  124. )
  125. # These commands SHOULD NOT include in the dockerfile if skip_init is True
  126. assert 'RUN apt update && apt install -y wget sudo' not in dockerfile_content
  127. assert (
  128. 'RUN /opendevin/miniforge3/bin/mamba install conda-forge::poetry python=3.11 -y'
  129. not in dockerfile_content
  130. )
  131. # These update commands SHOULD still in the dockerfile
  132. assert 'COPY ./code /opendevin/code' in dockerfile_content
  133. assert (
  134. '/opendevin/miniforge3/bin/mamba run -n base poetry install'
  135. in dockerfile_content
  136. )
  137. def test_get_runtime_image_repo_and_tag_eventstream():
  138. base_image = 'debian:11'
  139. img_repo, img_tag = get_runtime_image_repo_and_tag(base_image)
  140. assert (
  141. img_repo == f'{RUNTIME_IMAGE_REPO}'
  142. and img_tag == f'{OD_VERSION}_image_debian_tag_11'
  143. )
  144. base_image = 'nikolaik/python-nodejs:python3.11-nodejs22'
  145. img_repo, img_tag = get_runtime_image_repo_and_tag(base_image)
  146. assert (
  147. img_repo == f'{RUNTIME_IMAGE_REPO}'
  148. and img_tag
  149. == f'{OD_VERSION}_image_nikolaik___python-nodejs_tag_python3.11-nodejs22'
  150. )
  151. base_image = 'ubuntu'
  152. img_repo, img_tag = get_runtime_image_repo_and_tag(base_image)
  153. assert (
  154. img_repo == f'{RUNTIME_IMAGE_REPO}'
  155. and img_tag == f'{OD_VERSION}_image_ubuntu_tag_latest'
  156. )
  157. def test_build_runtime_image_from_scratch(temp_dir):
  158. base_image = 'debian:11'
  159. from_scratch_hash = prep_docker_build_folder(
  160. temp_dir,
  161. base_image,
  162. skip_init=False,
  163. )
  164. mock_runtime_builder = MagicMock()
  165. mock_runtime_builder.image_exists.return_value = False
  166. mock_runtime_builder.build.return_value = (
  167. f'{RUNTIME_IMAGE_REPO}:{from_scratch_hash}'
  168. )
  169. image_name = build_runtime_image(base_image, mock_runtime_builder)
  170. mock_runtime_builder.build.assert_called_once_with(
  171. path=ANY,
  172. tags=[
  173. f'{RUNTIME_IMAGE_REPO}:{from_scratch_hash}',
  174. f'{RUNTIME_IMAGE_REPO}:{OD_VERSION}_image_debian_tag_11',
  175. ],
  176. )
  177. assert image_name == f'{RUNTIME_IMAGE_REPO}:{from_scratch_hash}'
  178. def test_build_runtime_image_exact_hash_exist(temp_dir):
  179. base_image = 'debian:11'
  180. from_scratch_hash = prep_docker_build_folder(
  181. temp_dir,
  182. base_image,
  183. skip_init=False,
  184. )
  185. mock_runtime_builder = MagicMock()
  186. mock_runtime_builder.image_exists.return_value = True
  187. mock_runtime_builder.build.return_value = (
  188. f'{RUNTIME_IMAGE_REPO}:{from_scratch_hash}'
  189. )
  190. image_name = build_runtime_image(base_image, mock_runtime_builder)
  191. assert image_name == f'{RUNTIME_IMAGE_REPO}:{from_scratch_hash}'
  192. mock_runtime_builder.build.assert_not_called()
  193. @patch('opendevin.runtime.utils.runtime_build._build_sandbox_image')
  194. def test_build_runtime_image_exact_hash_not_exist(mock_build_sandbox_image, temp_dir):
  195. base_image = 'debian:11'
  196. repo, latest_image_tag = get_runtime_image_repo_and_tag(base_image)
  197. latest_image_name = f'{repo}:{latest_image_tag}'
  198. from_scratch_hash = prep_docker_build_folder(
  199. temp_dir,
  200. base_image,
  201. skip_init=False,
  202. )
  203. with tempfile.TemporaryDirectory() as temp_dir_2:
  204. non_from_scratch_hash = prep_docker_build_folder(
  205. temp_dir_2,
  206. base_image,
  207. skip_init=True,
  208. )
  209. mock_runtime_builder = MagicMock()
  210. # Set up mock_runtime_builder.image_exists to return False then True
  211. mock_runtime_builder.image_exists.side_effect = [False, True]
  212. with patch(
  213. 'opendevin.runtime.utils.runtime_build.prep_docker_build_folder'
  214. ) as mock_prep_docker_build_folder:
  215. mock_prep_docker_build_folder.side_effect = [
  216. from_scratch_hash,
  217. non_from_scratch_hash,
  218. ]
  219. image_name = build_runtime_image(base_image, mock_runtime_builder)
  220. mock_prep_docker_build_folder.assert_has_calls(
  221. [
  222. call(ANY, base_image=base_image, skip_init=False, extra_deps=None),
  223. call(
  224. ANY, base_image=latest_image_name, skip_init=True, extra_deps=None
  225. ),
  226. ]
  227. )
  228. mock_build_sandbox_image.assert_called_once_with(
  229. docker_folder=ANY,
  230. runtime_builder=mock_runtime_builder,
  231. target_image_repo=repo,
  232. target_image_hash_tag=from_scratch_hash,
  233. target_image_tag=latest_image_tag,
  234. )
  235. assert image_name == f'{repo}:{from_scratch_hash}'