conftest.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import os
  2. import random
  3. import shutil
  4. import stat
  5. import time
  6. from pathlib import Path
  7. import pytest
  8. from pytest import TempPathFactory
  9. from openhands.core.config import load_app_config
  10. from openhands.core.logger import openhands_logger as logger
  11. from openhands.events import EventStream
  12. from openhands.runtime.client.runtime import EventStreamRuntime
  13. from openhands.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
  14. from openhands.runtime.remote.runtime import RemoteRuntime
  15. from openhands.runtime.runtime import Runtime
  16. from openhands.storage import get_file_store
  17. TEST_IN_CI = os.getenv('TEST_IN_CI', 'False').lower() in ['true', '1', 'yes']
  18. TEST_RUNTIME = os.getenv('TEST_RUNTIME', 'eventstream').lower()
  19. RUN_AS_OPENHANDS = os.getenv('RUN_AS_OPENHANDS', 'True').lower() in ['true', '1', 'yes']
  20. test_mount_path = ''
  21. project_dir = os.path.dirname(
  22. os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  23. )
  24. sandbox_test_folder = '/openhands/workspace'
  25. def _get_runtime_sid(runtime: Runtime):
  26. logger.debug(f'\nruntime.sid: {runtime.sid}')
  27. return runtime.sid
  28. def _get_host_folder(runtime: Runtime):
  29. return runtime.config.workspace_mount_path
  30. def _get_sandbox_folder(runtime: Runtime):
  31. sid = _get_runtime_sid(runtime)
  32. if sid:
  33. return Path(os.path.join(sandbox_test_folder, sid))
  34. return None
  35. def _remove_folder(folder: str) -> bool:
  36. success = False
  37. if folder and os.path.isdir(folder):
  38. try:
  39. os.rmdir(folder)
  40. success = True
  41. except OSError:
  42. try:
  43. shutil.rmtree(folder)
  44. success = True
  45. except OSError:
  46. pass
  47. logger.debug(f'\nCleanup: `{folder}`: ' + ('[OK]' if success else '[FAILED]'))
  48. return success
  49. def _close_test_runtime(runtime: Runtime):
  50. if isinstance(runtime, EventStreamRuntime):
  51. runtime.close(rm_all_containers=False)
  52. else:
  53. runtime.close()
  54. time.sleep(1)
  55. def _reset_pwd():
  56. global project_dir
  57. # Try to change back to project directory
  58. try:
  59. os.chdir(project_dir)
  60. logger.info(f'Changed back to project directory `{project_dir}')
  61. except Exception as e:
  62. logger.error(f'Failed to change back to project directory: {e}')
  63. # *****************************************************************************
  64. # *****************************************************************************
  65. @pytest.fixture(autouse=True)
  66. def print_method_name(request):
  67. print(
  68. '\n\n########################################################################'
  69. )
  70. print(f'Running test: {request.node.name}')
  71. print(
  72. '########################################################################\n\n'
  73. )
  74. @pytest.fixture
  75. def temp_dir(tmp_path_factory: TempPathFactory, request) -> str:
  76. """Creates a unique temporary directory.
  77. Upon finalization, the temporary directory and its content is removed.
  78. The cleanup function is also called upon KeyboardInterrupt.
  79. Parameters:
  80. - tmp_path_factory (TempPathFactory): A TempPathFactory class
  81. Returns:
  82. - str: The temporary directory path that was created
  83. """
  84. temp_dir = tmp_path_factory.mktemp(
  85. 'rt_' + str(random.randint(100000, 999999)), numbered=False
  86. )
  87. logger.info(f'\n*** {request.node.name}\n>> temp folder: {temp_dir}\n')
  88. # Set permissions to ensure the directory is writable and deletable
  89. os.chmod(temp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 permissions
  90. def cleanup():
  91. global project_dir
  92. os.chdir(project_dir)
  93. _remove_folder(temp_dir)
  94. request.addfinalizer(cleanup)
  95. return str(temp_dir)
  96. # Depending on TEST_RUNTIME, feed the appropriate box class(es) to the test.
  97. def get_box_classes():
  98. runtime = TEST_RUNTIME
  99. if runtime.lower() == 'eventstream':
  100. return [EventStreamRuntime]
  101. elif runtime.lower() == 'remote':
  102. return [RemoteRuntime]
  103. else:
  104. raise ValueError(f'Invalid runtime: {runtime}')
  105. def get_run_as_openhands():
  106. print(
  107. '\n\n########################################################################'
  108. )
  109. print('USER: ' + 'openhands' if RUN_AS_OPENHANDS else 'root')
  110. print(
  111. '########################################################################\n\n'
  112. )
  113. return [RUN_AS_OPENHANDS]
  114. @pytest.fixture(scope='module') # for xdist
  115. def runtime_setup_module():
  116. _reset_pwd()
  117. yield
  118. _reset_pwd()
  119. @pytest.fixture(scope='session') # not for xdist
  120. def runtime_setup_session():
  121. _reset_pwd()
  122. yield
  123. _reset_pwd()
  124. # This assures that all tests run together per runtime, not alternating between them,
  125. # which cause errors (especially outside GitHub actions).
  126. @pytest.fixture(scope='module', params=get_box_classes())
  127. def box_class(request):
  128. time.sleep(1)
  129. return request.param
  130. # TODO: We will change this to `run_as_user` when `ServerRuntime` is deprecated.
  131. # since `EventStreamRuntime` supports running as an arbitrary user.
  132. @pytest.fixture(scope='module', params=get_run_as_openhands())
  133. def run_as_openhands(request):
  134. time.sleep(1)
  135. return request.param
  136. @pytest.fixture(scope='module', params=None)
  137. def base_container_image(request):
  138. time.sleep(1)
  139. env_image = os.environ.get('SANDBOX_BASE_CONTAINER_IMAGE')
  140. if env_image:
  141. request.param = env_image
  142. else:
  143. if not hasattr(request, 'param'): # prevent runtime AttributeError
  144. request.param = None
  145. if request.param is None and hasattr(request.config, 'sandbox'):
  146. try:
  147. request.param = request.config.sandbox.getoption(
  148. '--base_container_image'
  149. )
  150. except ValueError:
  151. request.param = None
  152. if request.param is None:
  153. request.param = pytest.param(
  154. 'nikolaik/python-nodejs:python3.12-nodejs22',
  155. 'golang:1.23-bookworm',
  156. )
  157. print(f'Container image: {request.param}')
  158. return request.param
  159. def _load_runtime(
  160. temp_dir,
  161. box_class,
  162. run_as_openhands: bool = True,
  163. enable_auto_lint: bool = False,
  164. base_container_image: str | None = None,
  165. browsergym_eval_env: str | None = None,
  166. use_workspace: bool | None = None,
  167. force_rebuild_runtime: bool = False,
  168. runtime_startup_env_vars: dict[str, str] | None = None,
  169. ) -> Runtime:
  170. sid = 'rt_' + str(random.randint(100000, 999999))
  171. # AgentSkills need to be initialized **before** Jupyter
  172. # otherwise Jupyter will not access the proper dependencies installed by AgentSkills
  173. plugins = [AgentSkillsRequirement(), JupyterRequirement()]
  174. config = load_app_config()
  175. config.run_as_openhands = run_as_openhands
  176. config.sandbox.force_rebuild_runtime = force_rebuild_runtime
  177. # Folder where all tests create their own folder
  178. global test_mount_path
  179. if use_workspace:
  180. test_mount_path = os.path.join(config.workspace_base, 'rt')
  181. else:
  182. test_mount_path = os.path.join(
  183. temp_dir, sid
  184. ) # need a subfolder to avoid conflicts
  185. config.workspace_mount_path = test_mount_path
  186. # Mounting folder specific for this test inside the sandbox
  187. config.workspace_mount_path_in_sandbox = f'{sandbox_test_folder}/{sid}'
  188. print('\nPaths used:')
  189. print(f'use_host_network: {config.sandbox.use_host_network}')
  190. print(f'workspace_base: {config.workspace_base}')
  191. print(f'workspace_mount_path: {config.workspace_mount_path}')
  192. print(
  193. f'workspace_mount_path_in_sandbox: {config.workspace_mount_path_in_sandbox}\n'
  194. )
  195. config.sandbox.browsergym_eval_env = browsergym_eval_env
  196. config.sandbox.enable_auto_lint = enable_auto_lint
  197. if runtime_startup_env_vars is not None:
  198. config.sandbox.runtime_startup_env_vars = runtime_startup_env_vars
  199. if base_container_image is not None:
  200. config.sandbox.base_container_image = base_container_image
  201. config.sandbox.runtime_container_image = None
  202. file_store = get_file_store(config.file_store, config.file_store_path)
  203. event_stream = EventStream(sid, file_store)
  204. runtime = box_class(
  205. config=config,
  206. event_stream=event_stream,
  207. sid=sid,
  208. plugins=plugins,
  209. )
  210. time.sleep(2)
  211. return runtime
  212. # Export necessary function
  213. __all__ = [
  214. '_load_runtime',
  215. '_get_host_folder',
  216. '_get_sandbox_folder',
  217. '_remove_folder',
  218. ]