Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

225 lines
6.2 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for E2BEnvironment."""
from unittest import mock
from e2b import CommandExitException
from e2b import CommandResult
from e2b import FileNotFoundException
from e2b import TimeoutException
from google.adk.integrations.e2b._e2b_environment import E2BEnvironment
import pytest
def _make_sandbox(*, running: bool = True) -> mock.MagicMock:
"""Build a mock AsyncSandbox with async method stubs."""
sandbox = mock.MagicMock(name='AsyncSandbox')
sandbox.is_running = mock.AsyncMock(return_value=running)
sandbox.set_timeout = mock.AsyncMock()
sandbox.kill = mock.AsyncMock(return_value=True)
sandbox.commands.run = mock.AsyncMock()
sandbox.files.read = mock.AsyncMock()
sandbox.files.write = mock.AsyncMock()
return sandbox
@pytest.fixture(name='sandbox')
def _sandbox() -> mock.MagicMock:
return _make_sandbox()
@pytest.fixture(name='create_patch')
def _create_patch(sandbox: mock.MagicMock):
"""Patch AsyncSandbox.create to return the mock sandbox."""
with mock.patch(
'e2b.AsyncSandbox.create', new=mock.AsyncMock(return_value=sandbox)
) as create:
yield create
@pytest.mark.asyncio
async def test_initialize_creates_sandbox(create_patch, sandbox):
env = E2BEnvironment(image='custom', timeout=120, env_vars={'A': '1'})
assert env.is_initialized is False
await env.initialize()
assert env.is_initialized is True
create_patch.assert_awaited_once()
_, kwargs = create_patch.call_args
assert kwargs['template'] == 'custom'
assert kwargs['timeout'] == 120
assert kwargs['envs'] == {'A': '1'}
assert env._sandbox is sandbox
@pytest.mark.asyncio
async def test_initialize_is_idempotent(create_patch, sandbox):
env = E2BEnvironment()
await env.initialize()
await env.initialize()
create_patch.assert_awaited_once()
@pytest.mark.asyncio
async def test_close_kills_sandbox_and_is_idempotent(create_patch, sandbox):
env = E2BEnvironment()
await env.initialize()
assert env.is_initialized is True
await env.close()
sandbox.kill.assert_awaited_once()
assert env._sandbox is None
assert env.is_initialized is False
# Second close is a no-op.
await env.close()
sandbox.kill.assert_awaited_once()
@pytest.mark.asyncio
async def test_working_dir_requires_initialize():
env = E2BEnvironment()
with pytest.raises(RuntimeError):
_ = env.working_dir
@pytest.mark.asyncio
async def test_execute_before_initialize_raises():
env = E2BEnvironment()
with pytest.raises(RuntimeError):
await env.execute('echo hi')
@pytest.mark.asyncio
async def test_execute_success(create_patch, sandbox):
sandbox.commands.run.return_value = CommandResult(
stdout='out', stderr='err', exit_code=0, error=None
)
env = E2BEnvironment()
await env.initialize()
result = await env.execute('echo out')
assert result.exit_code == 0
assert result.stdout == 'out'
assert result.stderr == 'err'
assert result.timed_out is False
sandbox.set_timeout.assert_awaited() # keepalive
@pytest.mark.asyncio
async def test_execute_nonzero_exit_is_normal_result(create_patch, sandbox):
exc = CommandExitException(
stdout='partial', stderr='boom', exit_code=2, error='failed'
)
sandbox.commands.run.side_effect = exc
env = E2BEnvironment()
await env.initialize()
result = await env.execute('false')
assert result.exit_code == 2
assert result.stdout == 'partial'
assert result.stderr == 'boom'
assert result.timed_out is False
@pytest.mark.asyncio
async def test_execute_timeout(create_patch, sandbox):
sandbox.commands.run.side_effect = TimeoutException('too slow')
env = E2BEnvironment()
await env.initialize()
result = await env.execute('sleep 999')
assert result.timed_out is True
@pytest.mark.asyncio
async def test_read_file_returns_bytes(create_patch, sandbox):
sandbox.files.read.return_value = b'data'
env = E2BEnvironment()
await env.initialize()
data = await env.read_file('notes.txt')
assert data == b'data'
sandbox.files.read.assert_awaited_once_with(
'/home/user/notes.txt', format='bytes'
)
@pytest.mark.asyncio
async def test_read_file_absolute_path_passthrough(create_patch, sandbox):
sandbox.files.read.return_value = b'x'
env = E2BEnvironment()
await env.initialize()
await env.read_file('/etc/hostname')
sandbox.files.read.assert_awaited_once_with('/etc/hostname', format='bytes')
@pytest.mark.asyncio
async def test_read_file_missing_raises(create_patch, sandbox):
sandbox.files.read.side_effect = FileNotFoundException('nope')
env = E2BEnvironment()
await env.initialize()
with pytest.raises(FileNotFoundError):
await env.read_file('missing.txt')
@pytest.mark.asyncio
async def test_write_file_resolves_relative_path(create_patch, sandbox):
env = E2BEnvironment()
await env.initialize()
await env.write_file('sub/out.txt', 'hello')
sandbox.files.write.assert_awaited_once_with(
'/home/user/sub/out.txt', 'hello'
)
@pytest.mark.asyncio
async def test_keepalive_extends_timeout_when_running(create_patch, sandbox):
sandbox.files.read.return_value = b'1'
env = E2BEnvironment(timeout=200)
await env.initialize()
await env.read_file('a.txt')
sandbox.set_timeout.assert_awaited_with(200)
@pytest.mark.asyncio
async def test_lazy_recreate_when_expired(sandbox):
expired = _make_sandbox(running=False)
fresh = _make_sandbox(running=True)
fresh.files.read.return_value = b'fresh'
with mock.patch(
'e2b.AsyncSandbox.create',
new=mock.AsyncMock(side_effect=[expired, fresh]),
) as create:
env = E2BEnvironment()
await env.initialize() # -> expired
data = await env.read_file('a.txt') # detects dead, recreates -> fresh
assert data == b'fresh'
assert create.await_count == 2
assert env._sandbox is fresh
expired.set_timeout.assert_not_awaited()