e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
237 lines
8.2 KiB
Python
237 lines
8.2 KiB
Python
#
|
|
# Copyright 2025 Alibaba Group Holding Ltd.
|
|
#
|
|
# 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 ``PoolConfig`` and ``AsyncPoolConfig`` validation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
|
|
from opensandbox.config.connection import ConnectionConfig
|
|
from opensandbox.config.connection_sync import ConnectionConfigSync
|
|
from opensandbox.pool import (
|
|
AsyncPoolConfig,
|
|
InMemoryAsyncPoolStateStore,
|
|
InMemoryPoolStateStore,
|
|
PoolConfig,
|
|
PoolCreationSpec,
|
|
PooledSandboxCreateContext,
|
|
SandboxPoolAsync,
|
|
)
|
|
from opensandbox.sandbox import Sandbox
|
|
from opensandbox.sync.pool import SandboxPoolSync
|
|
from opensandbox.sync.sandbox import SandboxSync
|
|
|
|
|
|
def _sync_kwargs() -> dict[str, object]:
|
|
return {
|
|
"pool_name": "test",
|
|
"max_idle": 1,
|
|
"state_store": InMemoryPoolStateStore(),
|
|
"connection_config": ConnectionConfigSync(),
|
|
"creation_spec": PoolCreationSpec(image="ubuntu:22.04"),
|
|
}
|
|
|
|
|
|
def _async_kwargs() -> dict[str, object]:
|
|
return {
|
|
"pool_name": "test",
|
|
"max_idle": 1,
|
|
"state_store": InMemoryAsyncPoolStateStore(),
|
|
"connection_config": ConnectionConfig(),
|
|
"creation_spec": PoolCreationSpec(image="ubuntu:22.04"),
|
|
}
|
|
|
|
|
|
def test_default_acquire_min_remaining_ttl_is_60s_for_long_idle_timeout() -> None:
|
|
# 24h idle_timeout caps at 60s.
|
|
config = PoolConfig(**_sync_kwargs()) # type: ignore[arg-type]
|
|
assert config.acquire_min_remaining_ttl == timedelta(seconds=60)
|
|
|
|
|
|
def test_async_default_acquire_min_remaining_ttl_is_60s_for_long_idle_timeout() -> None:
|
|
config = AsyncPoolConfig(**_async_kwargs()) # type: ignore[arg-type]
|
|
assert config.acquire_min_remaining_ttl == timedelta(seconds=60)
|
|
|
|
|
|
def test_default_acquire_min_remaining_ttl_scales_for_short_idle_timeout() -> None:
|
|
# idle_timeout=30s ⇒ default = min(60s, 30s/2) = 15s. Existing users with short
|
|
# idle timeouts must not get a config-time error from a hidden 60s default.
|
|
config = PoolConfig(**_sync_kwargs(), idle_timeout=timedelta(seconds=30)) # type: ignore[arg-type]
|
|
assert config.acquire_min_remaining_ttl == timedelta(seconds=15)
|
|
|
|
|
|
def test_negative_acquire_min_remaining_ttl_rejected() -> None:
|
|
with pytest.raises(
|
|
ValueError, match="acquire_min_remaining_ttl must be non-negative"
|
|
):
|
|
PoolConfig(**_sync_kwargs(), acquire_min_remaining_ttl=timedelta(seconds=-1)) # type: ignore[arg-type]
|
|
|
|
|
|
def test_explicit_acquire_min_remaining_ttl_at_or_above_idle_timeout_rejected() -> None:
|
|
# The auto-default protects against this, but an explicit value above idle_timeout
|
|
# still fails validation.
|
|
with pytest.raises(ValueError, match="strictly less than"):
|
|
PoolConfig( # type: ignore[arg-type]
|
|
**_sync_kwargs(),
|
|
idle_timeout=timedelta(seconds=30),
|
|
acquire_min_remaining_ttl=timedelta(seconds=30),
|
|
)
|
|
|
|
|
|
def test_async_explicit_acquire_min_remaining_ttl_at_or_above_idle_timeout_rejected() -> (
|
|
None
|
|
):
|
|
with pytest.raises(ValueError, match="strictly less than"):
|
|
AsyncPoolConfig( # type: ignore[arg-type]
|
|
**_async_kwargs(),
|
|
idle_timeout=timedelta(seconds=30),
|
|
acquire_min_remaining_ttl=timedelta(seconds=30),
|
|
)
|
|
|
|
|
|
def test_acquire_min_remaining_ttl_just_below_idle_timeout_accepted() -> None:
|
|
config = PoolConfig(
|
|
**_sync_kwargs(), # type: ignore[arg-type]
|
|
idle_timeout=timedelta(seconds=10),
|
|
acquire_min_remaining_ttl=timedelta(seconds=9),
|
|
)
|
|
assert config.acquire_min_remaining_ttl == timedelta(seconds=9)
|
|
assert config.idle_timeout == timedelta(seconds=10)
|
|
|
|
|
|
def test_zero_acquire_min_remaining_ttl_opts_out() -> None:
|
|
# Explicit Duration.ZERO ↔ legacy binary-expiry behavior; valid configuration.
|
|
config = PoolConfig(
|
|
**_sync_kwargs(), # type: ignore[arg-type]
|
|
acquire_min_remaining_ttl=timedelta(0),
|
|
)
|
|
assert config.acquire_min_remaining_ttl == timedelta(0)
|
|
|
|
|
|
def test_sync_pool_config_positional_owner_id_stays_compatible() -> None:
|
|
kwargs = _sync_kwargs()
|
|
|
|
config = PoolConfig(
|
|
kwargs["pool_name"], # type: ignore[arg-type]
|
|
kwargs["max_idle"], # type: ignore[arg-type]
|
|
kwargs["state_store"], # type: ignore[arg-type]
|
|
kwargs["connection_config"], # type: ignore[arg-type]
|
|
kwargs["creation_spec"], # type: ignore[arg-type]
|
|
"owner-1",
|
|
)
|
|
|
|
assert config.owner_id == "owner-1"
|
|
assert config.sandbox_creator is None
|
|
|
|
|
|
def test_async_pool_config_positional_owner_id_stays_compatible() -> None:
|
|
kwargs = _async_kwargs()
|
|
|
|
config = AsyncPoolConfig(
|
|
kwargs["pool_name"], # type: ignore[arg-type]
|
|
kwargs["max_idle"], # type: ignore[arg-type]
|
|
kwargs["state_store"], # type: ignore[arg-type]
|
|
kwargs["connection_config"], # type: ignore[arg-type]
|
|
kwargs["creation_spec"], # type: ignore[arg-type]
|
|
"owner-1",
|
|
)
|
|
|
|
assert config.owner_id == "owner-1"
|
|
assert config.sandbox_creator is None
|
|
|
|
|
|
def test_pool_facade_sandbox_creator_is_appended_after_factories() -> None:
|
|
sync_params = list(inspect.signature(SandboxPoolSync).parameters)
|
|
async_params = list(inspect.signature(SandboxPoolAsync).parameters)
|
|
|
|
assert sync_params[-3:] == [
|
|
"sandbox_manager_factory",
|
|
"sandbox_factory",
|
|
"sandbox_creator",
|
|
]
|
|
assert async_params[-3:] == [
|
|
"sandbox_manager_factory",
|
|
"sandbox_factory",
|
|
"sandbox_creator",
|
|
]
|
|
|
|
|
|
def test_sync_pool_config_keeps_sandbox_creator() -> None:
|
|
def creator(_context: PooledSandboxCreateContext) -> SandboxSync:
|
|
raise AssertionError("not called")
|
|
|
|
config = PoolConfig(
|
|
**_sync_kwargs(), # type: ignore[arg-type]
|
|
sandbox_creator=creator,
|
|
)
|
|
|
|
assert config.sandbox_creator is creator
|
|
|
|
|
|
def test_async_pool_config_keeps_sandbox_creator() -> None:
|
|
async def creator(_context: PooledSandboxCreateContext) -> Sandbox:
|
|
raise AssertionError("not called")
|
|
|
|
config = AsyncPoolConfig(
|
|
**_async_kwargs(), # type: ignore[arg-type]
|
|
sandbox_creator=creator,
|
|
)
|
|
|
|
assert config.sandbox_creator is creator
|
|
|
|
|
|
def test_sync_pool_facade_forwards_acquire_min_remaining_ttl() -> None:
|
|
"""``SandboxPoolSync.__init__`` exposes the new threshold and forwards it to the config.
|
|
|
|
Without the constructor kwarg, users with ``idle_timeout <= 60s`` hit the hidden
|
|
default and get a hard validation error with no way to override.
|
|
"""
|
|
from opensandbox.sync.pool import SandboxPoolSync
|
|
|
|
pool = SandboxPoolSync(
|
|
pool_name="test",
|
|
max_idle=1,
|
|
state_store=InMemoryPoolStateStore(),
|
|
connection_config=ConnectionConfigSync(),
|
|
creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
|
|
idle_timeout=timedelta(seconds=30),
|
|
acquire_min_remaining_ttl=timedelta(seconds=10),
|
|
)
|
|
|
|
assert pool._config.acquire_min_remaining_ttl == timedelta(seconds=10)
|
|
assert pool._config.idle_timeout == timedelta(seconds=30)
|
|
|
|
|
|
def test_async_pool_facade_forwards_acquire_min_remaining_ttl() -> None:
|
|
"""``SandboxPoolAsync.__init__`` exposes the new threshold and forwards it to the config."""
|
|
from opensandbox.pool_async import SandboxPoolAsync
|
|
|
|
pool = SandboxPoolAsync(
|
|
pool_name="test",
|
|
max_idle=1,
|
|
state_store=InMemoryAsyncPoolStateStore(),
|
|
connection_config=ConnectionConfig(),
|
|
creation_spec=PoolCreationSpec(image="ubuntu:22.04"),
|
|
idle_timeout=timedelta(seconds=30),
|
|
acquire_min_remaining_ttl=timedelta(seconds=10),
|
|
)
|
|
|
|
assert pool._config.acquire_min_remaining_ttl == timedelta(seconds=10)
|
|
assert pool._config.idle_timeout == timedelta(seconds=30)
|