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
348 lines
9.0 KiB
Go
348 lines
9.0 KiB
Go
// Copyright 2026 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.
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alibaba/OpenSandbox/sdks/sandbox/go"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func getConnectionConfig(t *testing.T) opensandbox.ConnectionConfig {
|
|
t.Helper()
|
|
|
|
domain := os.Getenv("OPENSANDBOX_TEST_DOMAIN")
|
|
if domain == "" {
|
|
domain = "localhost:8080"
|
|
}
|
|
|
|
protocol := os.Getenv("OPENSANDBOX_TEST_PROTOCOL")
|
|
if protocol == "" {
|
|
protocol = "http"
|
|
}
|
|
|
|
apiKey := os.Getenv("OPENSANDBOX_TEST_API_KEY")
|
|
if apiKey == "" {
|
|
apiKey = "e2e-test"
|
|
}
|
|
|
|
useProxy := os.Getenv("OPENSANDBOX_TEST_USE_SERVER_PROXY") == "true"
|
|
|
|
config := opensandbox.ConnectionConfig{
|
|
Domain: domain,
|
|
Protocol: protocol,
|
|
APIKey: apiKey,
|
|
UseServerProxy: useProxy,
|
|
}
|
|
|
|
if useProxy {
|
|
config.AuthHeader = "X-API-Key"
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func connectionConfigForStreaming(t *testing.T) opensandbox.ConnectionConfig {
|
|
t.Helper()
|
|
c := getConnectionConfig(t)
|
|
c.RequestTimeout = 3 * time.Minute
|
|
return c
|
|
}
|
|
|
|
func getSandboxImage() string {
|
|
if img := os.Getenv("OPENSANDBOX_SANDBOX_DEFAULT_IMAGE"); img != "" {
|
|
return img
|
|
}
|
|
return "python:3.11-slim"
|
|
}
|
|
|
|
func createTestSandbox(t *testing.T) (context.Context, *opensandbox.Sandbox) {
|
|
t.Helper()
|
|
config := connectionConfigForStreaming(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
t.Cleanup(cancel)
|
|
|
|
sb, err := opensandbox.CreateSandbox(ctx, config, opensandbox.SandboxCreateOptions{
|
|
Image: getSandboxImage(),
|
|
Env: map[string]string{"EXECD_API_GRACE_SHUTDOWN": "3s", "EXECD_JUPYTER_IDLE_POLL_INTERVAL": "200ms"},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { sb.Kill(context.Background()) })
|
|
return ctx, sb
|
|
}
|
|
|
|
func newExecdClientForSandbox(t *testing.T, ctx context.Context, sb *opensandbox.Sandbox) *opensandbox.ExecdClient {
|
|
t.Helper()
|
|
|
|
endpoint, err := sb.GetEndpoint(ctx, opensandbox.DefaultExecdPort)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, endpoint.Endpoint)
|
|
|
|
execdURL := endpoint.Endpoint
|
|
if !strings.HasPrefix(execdURL, "http") {
|
|
execdURL = "http://" + execdURL
|
|
}
|
|
execdURL = strings.Replace(execdURL, "host.docker.internal", "localhost", 1)
|
|
|
|
token := ""
|
|
if endpoint.Headers != nil {
|
|
token = endpoint.Headers["X-EXECD-ACCESS-TOKEN"]
|
|
}
|
|
return opensandbox.NewExecdClient(execdURL, token)
|
|
}
|
|
|
|
// ---------- Pool E2E helpers ----------
|
|
|
|
// Pool test constants aligned with Python E2E tests.
|
|
const (
|
|
poolMaxIdle = 2
|
|
poolReconcileInterval = 1 * time.Second
|
|
poolPrimaryLockTTL = 4 * time.Second
|
|
poolDrainTimeout = 300 * time.Millisecond
|
|
poolAwaitTimeout = 2 * time.Minute
|
|
)
|
|
|
|
func getE2eSandboxResource() opensandbox.ResourceLimits {
|
|
cpu := os.Getenv("OPENSANDBOX_E2E_SANDBOX_CPU")
|
|
if cpu == "" {
|
|
cpu = "1"
|
|
}
|
|
memory := os.Getenv("OPENSANDBOX_E2E_SANDBOX_MEMORY")
|
|
if memory == "" {
|
|
memory = "2Gi"
|
|
}
|
|
return opensandbox.ResourceLimits{"cpu": cpu, "memory": memory}
|
|
}
|
|
|
|
func poolTag(prefix string) string {
|
|
b := make([]byte, 4)
|
|
_, _ = rand.Read(b)
|
|
return prefix + "-" + hex.EncodeToString(b)
|
|
}
|
|
|
|
type poolCreateOpts struct {
|
|
warmupConcurrency int
|
|
warmupSandboxPreparer func(ctx context.Context, sb *opensandbox.Sandbox) error
|
|
connectionConfig *opensandbox.ConnectionConfig
|
|
degradedThreshold int
|
|
warmupReadyTimeout time.Duration
|
|
acquireReadyTimeout time.Duration
|
|
primaryLockTTL time.Duration
|
|
reconcileInterval time.Duration
|
|
}
|
|
|
|
func createTestPool(
|
|
t *testing.T,
|
|
poolName, ownerID string,
|
|
store opensandbox.PoolStateStore,
|
|
tag string,
|
|
maxIdle int,
|
|
opts *poolCreateOpts,
|
|
) *opensandbox.DefaultSandboxPool {
|
|
t.Helper()
|
|
|
|
connCfg := connectionConfigForStreaming(t)
|
|
warmupConc := 1
|
|
degradedThresh := 3
|
|
warmupReady := 30 * time.Second
|
|
acquireReady := 30 * time.Second
|
|
lockTTL := poolPrimaryLockTTL
|
|
reconcileInt := poolReconcileInterval
|
|
var preparer func(ctx context.Context, sb *opensandbox.Sandbox) error
|
|
|
|
if opts != nil {
|
|
if opts.warmupConcurrency > 0 {
|
|
warmupConc = opts.warmupConcurrency
|
|
}
|
|
if opts.degradedThreshold > 0 {
|
|
degradedThresh = opts.degradedThreshold
|
|
}
|
|
if opts.warmupReadyTimeout > 0 {
|
|
warmupReady = opts.warmupReadyTimeout
|
|
}
|
|
if opts.acquireReadyTimeout > 0 {
|
|
acquireReady = opts.acquireReadyTimeout
|
|
}
|
|
if opts.primaryLockTTL > 0 {
|
|
lockTTL = opts.primaryLockTTL
|
|
}
|
|
if opts.reconcileInterval > 0 {
|
|
reconcileInt = opts.reconcileInterval
|
|
}
|
|
if opts.connectionConfig != nil {
|
|
connCfg = *opts.connectionConfig
|
|
}
|
|
preparer = opts.warmupSandboxPreparer
|
|
}
|
|
|
|
builder := opensandbox.NewSandboxPoolBuilder().
|
|
PoolName(poolName).
|
|
OwnerID(ownerID).
|
|
MaxIdle(maxIdle).
|
|
WarmupConcurrency(warmupConc).
|
|
StateStore(store).
|
|
ConnectionConfig(connCfg).
|
|
CreationSpec(opensandbox.PoolCreationSpec{
|
|
Image: getSandboxImage(),
|
|
Entrypoint: []string{"tail", "-f", "/dev/null"},
|
|
Metadata: map[string]string{"tag": tag, "suite": "sandbox-pool-go-e2e"},
|
|
Env: map[string]string{
|
|
"E2E_TEST": "true",
|
|
"EXECD_API_GRACE_SHUTDOWN": "3s",
|
|
"EXECD_JUPYTER_IDLE_POLL_INTERVAL": "1s",
|
|
},
|
|
ResourceLimits: getE2eSandboxResource(),
|
|
}).
|
|
ReconcileInterval(reconcileInt).
|
|
PrimaryLockTTL(lockTTL).
|
|
DrainTimeout(poolDrainTimeout).
|
|
DegradedThreshold(degradedThresh).
|
|
WarmupReadyTimeout(warmupReady).
|
|
AcquireReadyTimeout(acquireReady)
|
|
|
|
if preparer != nil {
|
|
builder = builder.WarmupSandboxPreparer(preparer)
|
|
}
|
|
|
|
pool, err := builder.Build()
|
|
require.NoError(t, err, "pool build failed")
|
|
return pool
|
|
}
|
|
|
|
func eventually(t *testing.T, description string, condition func() bool, timeout, interval time.Duration) {
|
|
t.Helper()
|
|
if timeout == 0 {
|
|
timeout = poolAwaitTimeout
|
|
}
|
|
if interval == 0 {
|
|
interval = 1 * time.Second
|
|
}
|
|
deadline := time.After(timeout)
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatalf("timed out waiting for: %s", description)
|
|
case <-ticker.C:
|
|
if condition() {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanupPool(pool *opensandbox.DefaultSandboxPool) {
|
|
ctx := context.Background()
|
|
_ = pool.Resize(ctx, 0)
|
|
_, _ = pool.ReleaseAllIdle(ctx)
|
|
// Graceful shutdown first: waits for in-flight warmup to finish so
|
|
// orphan sandboxes are tracked and cleaned. Fall back to non-graceful.
|
|
_ = pool.Shutdown(ctx, true)
|
|
_ = pool.Shutdown(ctx, false)
|
|
}
|
|
|
|
func cleanupBorrowed(sandboxes []*opensandbox.Sandbox) {
|
|
for _, sb := range sandboxes {
|
|
_ = sb.Kill(context.Background())
|
|
_ = sb.Close()
|
|
}
|
|
}
|
|
|
|
func cleanupTaggedSandboxes(t *testing.T, tag string) {
|
|
t.Helper()
|
|
mgr := opensandbox.NewSandboxManager(getConnectionConfig(t))
|
|
defer mgr.Close()
|
|
ctx := context.Background()
|
|
// Brief wait for any in-flight sandbox creation to land before scanning.
|
|
time.Sleep(500 * time.Millisecond)
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
result, err := mgr.ListSandboxInfos(ctx, opensandbox.ListOptions{
|
|
Metadata: map[string]string{"tag": tag},
|
|
PageSize: 50,
|
|
})
|
|
if err != nil || len(result.Items) == 0 {
|
|
return
|
|
}
|
|
for _, info := range result.Items {
|
|
_ = mgr.KillSandbox(ctx, info.ID)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func countTaggedSandboxes(t *testing.T, tag string) int {
|
|
t.Helper()
|
|
mgr := opensandbox.NewSandboxManager(getConnectionConfig(t))
|
|
defer mgr.Close()
|
|
result, err := mgr.ListSandboxInfos(context.Background(), opensandbox.ListOptions{
|
|
Metadata: map[string]string{"tag": tag},
|
|
PageSize: 50,
|
|
})
|
|
if err != nil {
|
|
t.Logf("countTaggedSandboxes: list error: %v", err)
|
|
return 0
|
|
}
|
|
return len(result.Items)
|
|
}
|
|
|
|
func brokenConnectionConfig() opensandbox.ConnectionConfig {
|
|
return opensandbox.ConnectionConfig{
|
|
Domain: "127.0.0.1:9",
|
|
Protocol: "http",
|
|
APIKey: "broken-e2e-test",
|
|
RequestTimeout: 1 * time.Second,
|
|
}
|
|
}
|
|
|
|
func newPoolManager(t *testing.T) *opensandbox.SandboxManager {
|
|
t.Helper()
|
|
mgr := opensandbox.NewSandboxManager(getConnectionConfig(t))
|
|
t.Cleanup(func() { mgr.Close() })
|
|
return mgr
|
|
}
|
|
|
|
func snapshotIdle(pool *opensandbox.DefaultSandboxPool) int {
|
|
snap, err := pool.Snapshot(context.Background())
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
return snap.IdleCount
|
|
}
|
|
|
|
func snapshotState(pool *opensandbox.DefaultSandboxPool) string {
|
|
snap, err := pool.Snapshot(context.Background())
|
|
if err != nil {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
return snap.HealthState.String()
|
|
}
|
|
|
|
func snapshotLifecycle(pool *opensandbox.DefaultSandboxPool) string {
|
|
snap, err := pool.Snapshot(context.Background())
|
|
if err != nil {
|
|
return fmt.Sprintf("error: %v", err)
|
|
}
|
|
return snap.LifecycleState.String()
|
|
}
|