Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

482 lines
14 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.
#
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from opensandbox.api.lifecycle.models.create_sandbox_response import (
CreateSandboxResponse as ApiCreateSandboxResponse,
)
from opensandbox.api.lifecycle.models.image_spec import ImageSpec as ApiImageSpec
from opensandbox.api.lifecycle.models.sandbox import Sandbox as ApiSandbox
from opensandbox.api.lifecycle.types import UNSET
from opensandbox.models import CredentialSubstitution
from opensandbox.models.execd import (
Execution,
ExecutionError,
ExecutionLogs,
ExecutionResult,
OutputMessage,
)
from opensandbox.models.filesystem import (
DirectoryListEntry,
EntryInfo,
MoveEntry,
WriteEntry,
)
from opensandbox.models.sandboxes import (
OSSFS,
PVC,
Host,
SandboxFilter,
SandboxImageAuth,
SandboxImageSpec,
SandboxInfo,
SandboxStatus,
Volume,
)
def test_sandbox_image_spec_supports_positional_image() -> None:
spec = SandboxImageSpec("python:3.11")
assert spec.image == "python:3.11"
def test_models_package_exports_credential_substitution() -> None:
substitution = CredentialSubstitution(
credential="client-secret",
placeholder="__client_secret__",
in_=["body", "query"],
)
assert substitution.model_dump(by_alias=True)["in"] == ["body", "query"]
def test_sandbox_image_spec_rejects_blank_image() -> None:
with pytest.raises(ValueError):
SandboxImageSpec(" ")
def test_api_image_spec_tolerates_omitted_auth() -> None:
spec = ApiImageSpec.from_dict({"uri": "python:3.11"})
assert spec.uri == "python:3.11"
assert spec.auth is UNSET
def test_api_create_sandbox_response_tolerates_omitted_optional_fields() -> None:
response = ApiCreateSandboxResponse.from_dict(
{
"id": "sandbox-1",
"status": {"state": "Running"},
"createdAt": "2025-01-01T00:00:00Z",
"entrypoint": ["/bin/sh"],
}
)
assert response.metadata is UNSET
assert response.expires_at is UNSET
assert response.status.last_transition_at is UNSET
def test_api_sandbox_tolerates_omitted_optional_fields() -> None:
sandbox = ApiSandbox.from_dict(
{
"id": "sandbox-1",
"image": {"uri": "python:3.11"},
"status": {"state": "Running"},
"entrypoint": ["/bin/sh"],
"createdAt": "2025-01-01T00:00:00Z",
}
)
assert sandbox.metadata is UNSET
assert sandbox.expires_at is UNSET
assert sandbox.status.last_transition_at is UNSET
def test_sandbox_image_auth_rejects_blank_username_and_password() -> None:
with pytest.raises(ValueError):
SandboxImageAuth(username=" ", password="x")
with pytest.raises(ValueError):
SandboxImageAuth(username="u", password=" ")
def test_sandbox_filter_validations() -> None:
SandboxFilter(page=0, page_size=1)
with pytest.raises(ValueError):
SandboxFilter(page=-1)
with pytest.raises(ValueError):
SandboxFilter(page_size=0)
def test_sandbox_status_and_info_alias_dump_is_stable() -> None:
status = SandboxStatus(
state="RUNNING", last_transition_at=datetime(2025, 1, 1, tzinfo=timezone.utc)
)
info = SandboxInfo(
id=str(__import__("uuid").uuid4()),
status=status,
entrypoint=["/bin/sh"],
expires_at=datetime(2025, 1, 2, tzinfo=timezone.utc),
created_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
image=SandboxImageSpec("python:3.11"),
metadata={"k": "v"},
)
dumped = info.model_dump(by_alias=True, mode="json")
assert "expires_at" in dumped
assert "created_at" in dumped
assert dumped["status"]["last_transition_at"].endswith(("Z", "+00:00"))
def test_sandbox_info_supports_manual_cleanup_expiration() -> None:
info = SandboxInfo(
id=str(__import__("uuid").uuid4()),
status=SandboxStatus(state="RUNNING"),
entrypoint=["/bin/sh"],
expires_at=None,
created_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
image=SandboxImageSpec("python:3.11"),
)
dumped = info.model_dump(by_alias=True, mode="json")
assert dumped["expires_at"] is None
def test_filesystem_models_aliases_and_validation() -> None:
m = MoveEntry(source="/a", destination="/b")
assert m.src == "/a"
assert m.dest == "/b"
info = EntryInfo(
path="/workspace/file.txt",
type="file",
mode=644,
owner="root",
group="root",
size=1,
modified_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
created_at=datetime(2025, 1, 1, tzinfo=timezone.utc),
)
assert info.entry_type == "file"
assert info.model_dump(by_alias=True)["type"] == "file"
list_entry = DirectoryListEntry(path="/workspace", depth=2)
assert list_entry.depth == 2
with pytest.raises(ValueError):
DirectoryListEntry(path="/workspace", depth=-1)
with pytest.raises(ValueError):
WriteEntry(path=" ", data="x")
# ============================================================================
# Volume Model Tests
# ============================================================================
def test_host_backend_requires_absolute_path() -> None:
backend = Host(path="/data/shared")
assert backend.path == "/data/shared"
with pytest.raises(ValueError, match="absolute path"):
Host(path="relative/path")
def test_host_backend_accepts_unix_root_path() -> None:
"""Unix root path '/' must be accepted."""
assert Host(path="/").path == "/"
def test_host_backend_accepts_unix_nested_path() -> None:
"""Unix nested absolute path must be accepted."""
assert Host(path="/mnt/host/project").path == "/mnt/host/project"
def test_host_backend_accepts_windows_backslash_path() -> None:
"""Windows drive path with backslashes must be accepted."""
backend = Host(path="D:\\sandbox-mnt\\ReMe")
assert backend.path == "D:\\sandbox-mnt\\ReMe"
def test_host_backend_accepts_windows_forward_slash_path() -> None:
"""Windows drive path with forward slashes must be accepted."""
backend = Host(path="D:/sandbox-mnt/ReMe")
assert backend.path == "D:/sandbox-mnt/ReMe"
def test_host_backend_accepts_windows_drive_root() -> None:
"""Windows drive root (e.g. 'Z:\\') must be accepted."""
assert Host(path="Z:\\").path == "Z:\\"
def test_host_backend_accepts_windows_lowercase_drive() -> None:
"""Lowercase drive letter must be accepted."""
assert Host(path="a:/lower").path == "a:/lower"
def test_host_backend_rejects_relative_path() -> None:
"""Relative path without leading separator must be rejected."""
with pytest.raises(ValueError, match="absolute path"):
Host(path="relative/path")
def test_host_backend_rejects_dot_relative_path() -> None:
"""Dot-relative paths must be rejected."""
with pytest.raises(ValueError, match="absolute path"):
Host(path="./local")
def test_host_backend_rejects_parent_traversal_path() -> None:
"""Parent-traversal paths must be rejected."""
with pytest.raises(ValueError, match="absolute path"):
Host(path="../parent")
def test_host_backend_rejects_empty_path() -> None:
"""Empty string must be rejected."""
with pytest.raises(ValueError, match="absolute path"):
Host(path="")
def test_pvc_backend_rejects_blank_claim_name() -> None:
backend = PVC(claimName="my-pvc")
assert backend.claim_name == "my-pvc"
with pytest.raises(ValueError, match="blank"):
PVC(claimName=" ")
def test_ossfs_backend_default_version_is_2_0() -> None:
backend = OSSFS(
bucket="bucket-test-3",
endpoint="oss-cn-hangzhou.aliyuncs.com",
accessKeyId="ak",
accessKeySecret="sk",
)
assert backend.version == "2.0"
def test_volume_with_host_backend() -> None:
vol = Volume(
name="data",
host=Host(path="/data/shared"),
mountPath="/mnt/data",
)
assert vol.name == "data"
assert vol.host is not None
assert vol.host.path == "/data/shared"
assert vol.pvc is None
assert vol.mount_path == "/mnt/data"
assert vol.read_only is False # default is read-write
assert vol.sub_path is None
def test_volume_with_pvc_backend() -> None:
vol = Volume(
name="models",
pvc=PVC(claimName="shared-models"),
mountPath="/mnt/models",
readOnly=True,
subPath="v1",
)
assert vol.name == "models"
assert vol.host is None
assert vol.pvc is not None
assert vol.pvc.claim_name == "shared-models"
assert vol.mount_path == "/mnt/models"
assert vol.read_only is True
assert vol.sub_path == "v1"
def test_volume_rejects_blank_name() -> None:
with pytest.raises(ValueError, match="blank"):
Volume(
name=" ",
host=Host(path="/data"),
mountPath="/mnt",
)
def test_volume_requires_absolute_mount_path() -> None:
with pytest.raises(ValueError, match="absolute path"):
Volume(
name="test",
host=Host(path="/data"),
mountPath="relative/path",
)
def test_volume_serialization_uses_aliases() -> None:
vol = Volume(
name="test",
pvc=PVC(claimName="my-pvc"),
mountPath="/mnt/test",
readOnly=True,
subPath="sub",
)
dumped = vol.model_dump(by_alias=True, mode="json")
assert "mountPath" in dumped
assert "readOnly" in dumped
assert "subPath" in dumped
assert dumped["pvc"]["claimName"] == "my-pvc"
assert dumped["readOnly"] is True
def test_volume_rejects_no_backend() -> None:
"""Volume must have exactly one backend specified."""
with pytest.raises(ValueError, match="none was provided"):
Volume(
name="test",
mountPath="/mnt/test",
)
def test_volume_rejects_multiple_backends() -> None:
"""Volume must have exactly one backend, not multiple."""
with pytest.raises(ValueError, match="multiple were provided"):
Volume(
name="test",
host=Host(path="/data"),
pvc=PVC(claimName="my-pvc"),
mountPath="/mnt/test",
)
# ============================================================================
# Execution __str__ and .text Tests
# ============================================================================
def _make_output(text: str, *, is_error: bool = False) -> OutputMessage:
return OutputMessage(text=text, timestamp=0, is_error=is_error)
def _make_result(text: str) -> ExecutionResult:
return ExecutionResult(text=text, timestamp=0)
def test_execution_str_stdout_only() -> None:
ex = Execution(
logs=ExecutionLogs(
stdout=[_make_output("hello"), _make_output("world")],
),
)
assert str(ex) == "hello\nworld"
def test_execution_str_with_stderr() -> None:
ex = Execution(
logs=ExecutionLogs(
stdout=[_make_output("ok")],
stderr=[_make_output("warn", is_error=True)],
),
)
assert str(ex) == "ok\n[stderr]\nwarn"
def test_execution_str_with_error() -> None:
ex = Execution(
error=ExecutionError(name="RuntimeError", value="boom", timestamp=0),
)
assert str(ex) == "[error] RuntimeError: boom"
def test_execution_str_empty() -> None:
ex = Execution()
assert str(ex) == ""
assert ex.complete is None
assert ex.exit_code is None
def test_execution_text_property() -> None:
ex = Execution(
logs=ExecutionLogs(
stdout=[_make_output("line1"), _make_output("line2")],
stderr=[_make_output("ignored", is_error=True)],
),
)
assert ex.text == "line1\nline2"
def test_execution_text_includes_results() -> None:
"""code-interpreter stores return values in result, not stdout."""
ex = Execution(
result=[_make_result("4")],
)
assert ex.text == "4"
assert str(ex) == "4"
def test_execution_text_combines_stdout_and_results() -> None:
ex = Execution(
logs=ExecutionLogs(
stdout=[_make_output("3.11.14")],
),
result=[_make_result("4")],
)
assert ex.text == "3.11.14\n4"
def test_execution_text_strips_trailing_newlines() -> None:
"""code-interpreter streaming sends chunks with trailing newlines."""
ex = Execution(
logs=ExecutionLogs(
stdout=[_make_output("1\n"), _make_output("2\n")],
),
)
assert ex.text == "1\n2"
assert str(ex) == "1\n2"
def test_isolated_binds_serialize_to_wire_format() -> None:
"""binds and uid_mode serialize to the execd wire format."""
from opensandbox.models import (
BindMount,
CreateIsolatedSessionRequest,
IsolatedWorkspaceSpec,
)
req = CreateIsolatedSessionRequest(
workspace=IsolatedWorkspaceSpec(path="/workspace", mode="rw"),
binds=[
BindMount(source="/data/in", dest="/mnt/in", readonly=True),
BindMount(source="/data/out"),
],
uid_mode="userns",
)
body = req.model_dump(exclude_none=True)
assert body["uid_mode"] == "userns"
assert body["binds"] == [
{"source": "/data/in", "dest": "/mnt/in", "readonly": True},
{"source": "/data/out"},
]
def test_isolated_binds_omitted_when_unset() -> None:
"""binds and uid_mode are omitted when unset (backward compatible)."""
from opensandbox.models import (
CreateIsolatedSessionRequest,
IsolatedWorkspaceSpec,
)
req = CreateIsolatedSessionRequest(
workspace=IsolatedWorkspaceSpec(path="/workspace"),
)
body = req.model_dump(exclude_none=True)
assert "binds" not in body
assert "uid_mode" not in body