mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
"""Test that the runtime import system is robust against broken third-party dependencies.
|
|
|
|
This test specifically addresses the issue where broken third-party runtime dependencies
|
|
(like runloop-api-client with incompatible httpx_aiohttp versions) would break the entire
|
|
OpenHands CLI and system.
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
def test_runtime_import_robustness():
|
|
"""Test that runtime import system is robust against broken dependencies."""
|
|
# Clear any cached runtime modules
|
|
modules_to_clear = [k for k in sys.modules.keys() if 'openhands.runtime' in k]
|
|
for module in modules_to_clear:
|
|
del sys.modules[module]
|
|
|
|
# Import the runtime module - should succeed even with broken third-party runtimes
|
|
try:
|
|
import openhands.runtime # noqa: F401
|
|
|
|
assert True
|
|
except Exception as e:
|
|
pytest.fail(f'Runtime import failed: {e}')
|
|
|
|
|
|
def test_get_runtime_cls_works():
|
|
"""Test that get_runtime_cls works even when third-party runtimes are broken."""
|
|
# Import the runtime module
|
|
import openhands.runtime
|
|
|
|
# Test that we can still get core runtime classes
|
|
docker_runtime = openhands.runtime.get_runtime_cls('docker')
|
|
assert docker_runtime is not None
|
|
|
|
local_runtime = openhands.runtime.get_runtime_cls('local')
|
|
assert local_runtime is not None
|
|
|
|
# Test that requesting a non-existent runtime raises appropriate error
|
|
with pytest.raises(ValueError, match='Runtime nonexistent not supported'):
|
|
openhands.runtime.get_runtime_cls('nonexistent')
|
|
|
|
|
|
def test_runtime_exception_handling():
|
|
"""Test that the runtime discovery code properly handles exceptions."""
|
|
# This test verifies that the fix in openhands/runtime/__init__.py
|
|
# properly catches all exceptions (not just ImportError) during
|
|
# third-party runtime discovery
|
|
|
|
import openhands.runtime
|
|
|
|
# The fact that we can import this module successfully means
|
|
# the exception handling is working correctly, even if there
|
|
# are broken third-party runtime dependencies
|
|
assert hasattr(openhands.runtime, 'get_runtime_cls')
|
|
assert hasattr(openhands.runtime, '_THIRD_PARTY_RUNTIME_CLASSES')
|
|
|
|
|
|
def test_runtime_import_exception_handling_behavior():
|
|
"""Test that runtime import handles ImportError silently but logs other exceptions."""
|
|
# Test the exception handling logic by simulating the exact code from runtime init
|
|
from io import StringIO
|
|
|
|
from openhands.core.logger import openhands_logger as logger
|
|
|
|
# Create a string buffer to capture log output
|
|
log_capture = StringIO()
|
|
handler = logging.StreamHandler(log_capture)
|
|
handler.setLevel(logging.WARNING)
|
|
|
|
# Add our test handler to the OpenHands logger
|
|
logger.addHandler(handler)
|
|
original_level = logger.level
|
|
logger.setLevel(logging.WARNING)
|
|
|
|
try:
|
|
# Test 1: ImportError should be handled silently (no logging)
|
|
module_path = 'third_party.runtime.impl.missing.missing_runtime'
|
|
try:
|
|
raise ImportError("No module named 'missing_library'")
|
|
except ImportError:
|
|
# This is the exact code from runtime init: just pass, no logging
|
|
pass
|
|
|
|
# Test 2: Other exceptions should be logged
|
|
module_path = 'third_party.runtime.impl.runloop.runloop_runtime'
|
|
try:
|
|
raise AttributeError(
|
|
"module 'httpx_aiohttp' has no attribute 'HttpxAiohttpClient'"
|
|
)
|
|
except ImportError:
|
|
# ImportError means the library is not installed (expected for optional dependencies)
|
|
pass
|
|
except Exception as e:
|
|
# Other exceptions mean the library is present but broken, which should be logged
|
|
# This is the exact code from runtime init
|
|
logger.warning(f'Failed to import third-party runtime {module_path}: {e}')
|
|
|
|
# Check the captured log output
|
|
log_output = log_capture.getvalue()
|
|
|
|
# Should contain the AttributeError warning
|
|
assert 'Failed to import third-party runtime' in log_output
|
|
assert 'HttpxAiohttpClient' in log_output
|
|
# Should NOT contain the ImportError message
|
|
assert 'missing_library' not in log_output
|
|
|
|
finally:
|
|
logger.removeHandler(handler)
|
|
logger.setLevel(original_level)
|
|
|
|
|
|
def test_import_error_handled_silently(caplog):
|
|
"""Test that ImportError is handled silently (no logging) as it means library is not installed."""
|
|
# Simulate the exact code path for ImportError
|
|
logging.getLogger('openhands.runtime')
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
# Simulate ImportError handling - this should NOT log anything
|
|
try:
|
|
raise ImportError("No module named 'optional_runtime_library'")
|
|
except ImportError:
|
|
# This is the exact code from runtime init: just pass, no logging
|
|
pass
|
|
|
|
# Check that NO warning was logged for ImportError
|
|
warning_records = [
|
|
record for record in caplog.records if record.levelname == 'WARNING'
|
|
]
|
|
assert len(warning_records) == 0, (
|
|
f'ImportError should not generate warnings, but got: {warning_records}'
|
|
)
|