chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
# 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 DaytonaEnvironment."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import daytona
|
||||
from daytona import CreateSandboxFromImageParams
|
||||
from daytona import CreateSandboxFromSnapshotParams
|
||||
from daytona import DaytonaError
|
||||
from daytona import DaytonaNotFoundError
|
||||
from google.adk.integrations.daytona._daytona_environment import DaytonaEnvironment
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_sandbox() -> mock.MagicMock:
|
||||
"""Build a mock AsyncSandbox with async method stubs."""
|
||||
sandbox = mock.MagicMock(name="AsyncSandbox")
|
||||
sandbox.delete = mock.AsyncMock()
|
||||
sandbox.process.exec = mock.AsyncMock()
|
||||
sandbox.fs.download_file = mock.AsyncMock()
|
||||
sandbox.fs.upload_file = mock.AsyncMock()
|
||||
sandbox.fs.create_folder = mock.AsyncMock()
|
||||
sandbox.refresh_activity = mock.AsyncMock()
|
||||
return sandbox
|
||||
|
||||
|
||||
@pytest.fixture(name="sandbox")
|
||||
def _sandbox() -> mock.MagicMock:
|
||||
return _make_sandbox()
|
||||
|
||||
|
||||
@pytest.fixture(name="daytona_patch")
|
||||
def _daytona_patch(sandbox: mock.MagicMock):
|
||||
"""Patch AsyncDaytona to return a mock client."""
|
||||
mock_client = mock.MagicMock(name="AsyncDaytona")
|
||||
mock_client.create = mock.AsyncMock(return_value=sandbox)
|
||||
|
||||
with mock.patch.object(daytona, "AsyncDaytona", autospec=True) as mock_class:
|
||||
mock_class.return_value = mock_client
|
||||
yield mock_class
|
||||
|
||||
|
||||
async def test_initialize_creates_sandbox(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment(image="custom-image", env_vars={"A": "1"})
|
||||
assert env.is_initialized is False
|
||||
await env.initialize()
|
||||
assert env.is_initialized is True
|
||||
|
||||
daytona_patch.assert_called_once()
|
||||
client = daytona_patch.return_value
|
||||
client.create.assert_awaited_once()
|
||||
|
||||
args, _ = client.create.call_args
|
||||
params = args[0]
|
||||
assert isinstance(params, CreateSandboxFromImageParams)
|
||||
assert params.image == "custom-image"
|
||||
assert params.env_vars == {"A": "1"}
|
||||
assert params.auto_stop_interval == 5
|
||||
assert params.auto_delete_interval == 0
|
||||
assert env._sandbox is sandbox
|
||||
|
||||
|
||||
async def test_initialize_creates_sandbox_default(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment(env_vars={"B": "2"})
|
||||
await env.initialize()
|
||||
|
||||
daytona_patch.assert_called_once()
|
||||
client = daytona_patch.return_value
|
||||
client.create.assert_awaited_once()
|
||||
|
||||
args, _ = client.create.call_args
|
||||
params = args[0]
|
||||
assert isinstance(params, CreateSandboxFromSnapshotParams)
|
||||
assert params.language == "python"
|
||||
assert params.env_vars == {"B": "2"}
|
||||
assert params.auto_stop_interval == 5
|
||||
assert params.auto_delete_interval == 0
|
||||
assert env._sandbox is sandbox
|
||||
|
||||
|
||||
async def test_initialize_is_idempotent(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
await env.initialize()
|
||||
client = daytona_patch.return_value
|
||||
client.create.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_close_deletes_sandbox_and_is_idempotent(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
assert env.is_initialized is True
|
||||
await env.close()
|
||||
sandbox.delete.assert_awaited_once()
|
||||
assert env._sandbox is None
|
||||
assert env.is_initialized is False
|
||||
|
||||
# Second close is a no-op.
|
||||
await env.close()
|
||||
sandbox.delete.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_working_dir_requires_initialize():
|
||||
env = DaytonaEnvironment()
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = env.working_dir
|
||||
|
||||
|
||||
async def test_execute_before_initialize_raises():
|
||||
env = DaytonaEnvironment()
|
||||
with pytest.raises(RuntimeError):
|
||||
await env.execute("echo hi")
|
||||
|
||||
|
||||
async def test_execute_success(daytona_patch, sandbox):
|
||||
class MockArtifacts:
|
||||
stdout = "out"
|
||||
|
||||
class MockResponse:
|
||||
exit_code = 0
|
||||
artifacts = MockArtifacts()
|
||||
|
||||
sandbox.process.exec.return_value = MockResponse()
|
||||
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
result = await env.execute("echo out")
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout == "out"
|
||||
assert result.stderr == ""
|
||||
assert result.timed_out is False
|
||||
sandbox.refresh_activity.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_execute_timeout(daytona_patch, sandbox):
|
||||
# Simulate a Daytona timeout error
|
||||
sandbox.process.exec.side_effect = DaytonaError("timeout occurred")
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
result = await env.execute("sleep 999")
|
||||
|
||||
assert result.timed_out is True
|
||||
|
||||
|
||||
async def test_read_file_returns_bytes(daytona_patch, sandbox):
|
||||
sandbox.fs.download_file.return_value = b"data"
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
data = await env.read_file("notes.txt")
|
||||
|
||||
assert data == b"data"
|
||||
sandbox.fs.download_file.assert_awaited_once_with("/workspaces/notes.txt")
|
||||
sandbox.refresh_activity.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_read_file_absolute_path_passthrough(daytona_patch, sandbox):
|
||||
sandbox.fs.download_file.return_value = b"x"
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
await env.read_file("/etc/hostname")
|
||||
|
||||
sandbox.fs.download_file.assert_awaited_once_with("/etc/hostname")
|
||||
|
||||
|
||||
async def test_read_file_missing_raises(daytona_patch, sandbox):
|
||||
sandbox.fs.download_file.return_value = None
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await env.read_file("missing.txt")
|
||||
|
||||
|
||||
async def test_write_file_resolves_relative_path(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
await env.write_file("sub/out.txt", "hello")
|
||||
sandbox.refresh_activity.assert_awaited_once()
|
||||
|
||||
sandbox.fs.upload_file.assert_awaited_once_with(
|
||||
b"hello", "/workspaces/sub/out.txt"
|
||||
)
|
||||
|
||||
|
||||
async def test_initialize_propagates_api_key_and_url(daytona_patch, sandbox):
|
||||
from daytona import DaytonaConfig
|
||||
|
||||
env = DaytonaEnvironment(api_key="my-key", api_url="my-url")
|
||||
await env.initialize()
|
||||
|
||||
daytona_patch.assert_called_once()
|
||||
_, kwargs = daytona_patch.call_args
|
||||
config = kwargs.get("config")
|
||||
assert isinstance(config, DaytonaConfig)
|
||||
assert config.api_key == "my-key"
|
||||
assert config.api_url == "my-url"
|
||||
|
||||
|
||||
async def test_write_file_creates_parent_directory(daytona_patch, sandbox):
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
await env.write_file("sub/nested/file.txt", "content")
|
||||
|
||||
sandbox.fs.create_folder.assert_has_calls([
|
||||
mock.call("/workspaces", mode="755"),
|
||||
mock.call("/workspaces/sub", mode="755"),
|
||||
mock.call("/workspaces/sub/nested", mode="755"),
|
||||
])
|
||||
sandbox.fs.upload_file.assert_awaited_once_with(
|
||||
b"content", "/workspaces/sub/nested/file.txt"
|
||||
)
|
||||
|
||||
|
||||
async def test_read_file_raises_file_not_found_on_daytona_not_found(
|
||||
daytona_patch, sandbox
|
||||
):
|
||||
sandbox.fs.download_file.side_effect = DaytonaNotFoundError("not found")
|
||||
env = DaytonaEnvironment()
|
||||
await env.initialize()
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await env.read_file("missing.txt")
|
||||
Reference in New Issue
Block a user