"""Tests for GUI launcher functionality.""" import os import subprocess from pathlib import Path from unittest.mock import MagicMock, patch import pytest from openhands_cli.gui_launcher import ( _format_docker_command_for_logging, check_docker_requirements, get_openhands_version, launch_gui_server, ) class TestFormatDockerCommand: """Test the Docker command formatting function.""" @pytest.mark.parametrize( "cmd,expected", [ ( ['docker', 'run', 'hello-world'], 'Running Docker command: docker run hello-world', ), ( ['docker', 'run', '-it', '--rm', '-p', '3000:3000', 'openhands:latest'], 'Running Docker command: docker run -it --rm -p 3000:3000 openhands:latest', ), ([], 'Running Docker command: '), ], ) def test_format_docker_command(self, cmd, expected): """Test formatting Docker commands.""" result = _format_docker_command_for_logging(cmd) assert result == expected class TestCheckDockerRequirements: """Test Docker requirements checking.""" @pytest.mark.parametrize( "which_return,run_side_effect,expected_result,expected_print_count", [ # Docker not installed (None, None, False, 2), # Docker daemon not running ('/usr/bin/docker', MagicMock(returncode=1), False, 2), # Docker timeout ('/usr/bin/docker', subprocess.TimeoutExpired('docker info', 10), False, 2), # Docker available ('/usr/bin/docker', MagicMock(returncode=0), True, 0), ], ) @patch('shutil.which') @patch('subprocess.run') def test_docker_requirements( self, mock_run, mock_which, which_return, run_side_effect, expected_result, expected_print_count ): """Test Docker requirements checking scenarios.""" mock_which.return_value = which_return if run_side_effect is not None: if isinstance(run_side_effect, Exception): mock_run.side_effect = run_side_effect else: mock_run.return_value = run_side_effect with patch('openhands_cli.gui_launcher.print_formatted_text') as mock_print: result = check_docker_requirements() assert result is expected_result assert mock_print.call_count == expected_print_count class TestGetOpenHandsVersion: """Test version retrieval.""" @pytest.mark.parametrize( "env_value,expected", [ (None, 'latest'), # No environment variable set ('1.2.3', '1.2.3'), # Environment variable set ], ) def test_version_retrieval(self, env_value, expected): """Test version retrieval from environment.""" if env_value: os.environ['OPENHANDS_VERSION'] = env_value result = get_openhands_version() assert result == expected class TestLaunchGuiServer: """Test GUI server launching.""" @patch('openhands_cli.gui_launcher.check_docker_requirements') @patch('openhands_cli.gui_launcher.print_formatted_text') def test_launch_gui_server_docker_not_available(self, mock_print, mock_check_docker): """Test that launch_gui_server exits when Docker is not available.""" mock_check_docker.return_value = False with pytest.raises(SystemExit) as exc_info: launch_gui_server() assert exc_info.value.code == 1 @pytest.mark.parametrize( "pull_side_effect,run_side_effect,expected_exit_code,mount_cwd,gpu", [ # Docker pull failure (subprocess.CalledProcessError(1, 'docker pull'), None, 1, False, False), # Docker run failure (MagicMock(returncode=0), subprocess.CalledProcessError(1, 'docker run'), 1, False, False), # KeyboardInterrupt during run (MagicMock(returncode=0), KeyboardInterrupt(), 0, False, False), # Success with mount_cwd (MagicMock(returncode=0), MagicMock(returncode=0), None, True, False), # Success with GPU (MagicMock(returncode=0), MagicMock(returncode=0), None, False, True), ], ) @patch('openhands_cli.gui_launcher.check_docker_requirements') @patch('openhands_cli.gui_launcher.ensure_config_dir_exists') @patch('openhands_cli.gui_launcher.get_openhands_version') @patch('subprocess.run') @patch('subprocess.check_output') @patch('pathlib.Path.cwd') @patch('openhands_cli.gui_launcher.print_formatted_text') def test_launch_gui_server_scenarios( self, mock_print, mock_cwd, mock_check_output, mock_run, mock_version, mock_config_dir, mock_check_docker, pull_side_effect, run_side_effect, expected_exit_code, mount_cwd, gpu, ): """Test various GUI server launch scenarios.""" # Setup mocks mock_check_docker.return_value = True mock_config_dir.return_value = Path('/home/user/.openhands') mock_version.return_value = 'latest' mock_check_output.return_value = '1000\n' mock_cwd.return_value = Path('/current/dir') # Configure subprocess.run side effects side_effects = [] if pull_side_effect is not None: if isinstance(pull_side_effect, Exception): side_effects.append(pull_side_effect) else: side_effects.append(pull_side_effect) if run_side_effect is not None: if isinstance(run_side_effect, Exception): side_effects.append(run_side_effect) else: side_effects.append(run_side_effect) mock_run.side_effect = side_effects # Test the function if expected_exit_code is not None: with pytest.raises(SystemExit) as exc_info: launch_gui_server(mount_cwd=mount_cwd, gpu=gpu) assert exc_info.value.code == expected_exit_code else: # Should not raise SystemExit for successful cases launch_gui_server(mount_cwd=mount_cwd, gpu=gpu) # Verify subprocess.run was called correctly assert mock_run.call_count == 2 # Pull and run commands # Check pull command pull_call = mock_run.call_args_list[0] pull_cmd = pull_call[0][0] assert pull_cmd[0:3] == ['docker', 'pull', 'docker.openhands.dev/openhands/runtime:latest-nikolaik'] # Check run command run_call = mock_run.call_args_list[1] run_cmd = run_call[0][0] assert run_cmd[0:2] == ['docker', 'run'] if mount_cwd: assert 'SANDBOX_VOLUMES=/current/dir:/workspace:rw' in ' '.join(run_cmd) assert 'SANDBOX_USER_ID=1000' in ' '.join(run_cmd) if gpu: assert '--gpus' in run_cmd assert 'all' in run_cmd assert 'SANDBOX_ENABLE_GPU=true' in ' '.join(run_cmd)