e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
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
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
301 lines
8.8 KiB
Go
301 lines
8.8 KiB
Go
// 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.
|
|
|
|
package execute
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
execdflag "github.com/alibaba/opensandbox/execd/pkg/flag"
|
|
)
|
|
|
|
// Create WebSocket test server
|
|
func createTestServer(t *testing.T, handleFunc func(conn *websocket.Conn)) *httptest.Server {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Validate request path
|
|
if !strings.HasPrefix(r.URL.Path, "/api/kernels/") {
|
|
t.Errorf("expected path to start with '/api/kernels/', got '%s'", r.URL.Path)
|
|
}
|
|
if !strings.HasSuffix(r.URL.Path, "/channels") {
|
|
t.Errorf("expected path to end with '/channels', got '%s'", r.URL.Path)
|
|
}
|
|
|
|
// Upgrade HTTP connection to WebSocket
|
|
upgrader := websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|
}
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to upgrade to WebSocket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Handle WebSocket connection
|
|
handleFunc(conn)
|
|
}))
|
|
|
|
return server
|
|
}
|
|
|
|
// Test streaming code execution
|
|
func TestExecuteCodeStream(t *testing.T) {
|
|
// Spin up mock WebSocket server
|
|
server := createTestServer(t, func(conn *websocket.Conn) {
|
|
// Read execution request
|
|
var executeRequest Message
|
|
err := conn.ReadJSON(&executeRequest)
|
|
if err != nil {
|
|
t.Fatalf("failed to read execution request: %v", err)
|
|
}
|
|
|
|
// Send multiple stream messages
|
|
for i := 0; i < 3; i++ {
|
|
streamContent, _ := json.Marshal(StreamOutput{
|
|
Name: StreamStdout,
|
|
Text: "Line " + string(rune('0'+i)) + "\n",
|
|
})
|
|
|
|
streamMsg := Message{
|
|
Header: Header{
|
|
MessageID: "stream-msg-id-" + string(rune('0'+i)),
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgStream),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(streamContent),
|
|
}
|
|
conn.WriteJSON(streamMsg)
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Send execution result
|
|
resultContent, _ := json.Marshal(ExecuteResult{
|
|
ExecutionCount: 1,
|
|
Data: map[string]interface{}{
|
|
"text/plain": "Completed",
|
|
},
|
|
Metadata: map[string]interface{}{},
|
|
})
|
|
|
|
executeResultMsg := Message{
|
|
Header: Header{
|
|
MessageID: "result-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgExecuteResult),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(resultContent),
|
|
}
|
|
conn.WriteJSON(executeResultMsg)
|
|
|
|
// Send status message
|
|
statusContent, _ := json.Marshal(StatusUpdate{
|
|
ExecutionState: StateIdle,
|
|
})
|
|
|
|
statusMsg := Message{
|
|
Header: Header{
|
|
MessageID: "status-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgStatus),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(statusContent),
|
|
}
|
|
conn.WriteJSON(statusMsg)
|
|
})
|
|
defer server.Close()
|
|
|
|
// Convert HTTP URL to WebSocket URL
|
|
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/api/kernels/test-kernel-id/channels"
|
|
|
|
// Create executor client
|
|
executor := NewExecutor(wsURL, nil)
|
|
|
|
// Connect to WebSocket
|
|
err := executor.Connect()
|
|
if err != nil {
|
|
t.Fatalf("failed to connect to WebSocket: %v", err)
|
|
}
|
|
defer executor.Disconnect()
|
|
|
|
// Execute code in streaming mode
|
|
resultChan := make(chan *ExecutionResult, 10)
|
|
err = executor.ExecuteCodeStream("for i in range(3):\n print(f'Line {i}')", resultChan)
|
|
if err != nil {
|
|
t.Fatalf("failed to start streaming execution: %v", err)
|
|
}
|
|
|
|
// Receive and verify stream results
|
|
resultCount := 0
|
|
for result := range resultChan {
|
|
if result == nil {
|
|
break
|
|
}
|
|
resultCount++
|
|
}
|
|
|
|
// Should receive at least 4 results (3 stream outputs + 1 final result)
|
|
if resultCount < 4 {
|
|
t.Errorf("expected at least 4 results, got %d", resultCount)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCodeStreamWaitsForLateExecuteResultUsingConfiguredPollInterval(t *testing.T) {
|
|
previousPollInterval := execdflag.JupyterIdlePollInterval
|
|
execdflag.JupyterIdlePollInterval = time.Millisecond
|
|
t.Cleanup(func() {
|
|
execdflag.JupyterIdlePollInterval = previousPollInterval
|
|
})
|
|
|
|
server := createTestServer(t, func(conn *websocket.Conn) {
|
|
var executeRequest Message
|
|
err := conn.ReadJSON(&executeRequest)
|
|
if err != nil {
|
|
t.Fatalf("failed to read execution request: %v", err)
|
|
}
|
|
|
|
statusContent, _ := json.Marshal(StatusUpdate{ExecutionState: StateIdle})
|
|
statusMsg := Message{
|
|
Header: Header{
|
|
MessageID: "status-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgStatus),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(statusContent),
|
|
}
|
|
require.NoError(t, conn.WriteJSON(statusMsg))
|
|
|
|
time.Sleep(15 * time.Millisecond)
|
|
|
|
resultContent, _ := json.Marshal(ExecuteResult{
|
|
ExecutionCount: 1,
|
|
Data: map[string]interface{}{
|
|
"text/plain": "Completed late",
|
|
},
|
|
Metadata: map[string]interface{}{},
|
|
})
|
|
executeResultMsg := Message{
|
|
Header: Header{
|
|
MessageID: "result-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgExecuteResult),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(resultContent),
|
|
}
|
|
require.NoError(t, conn.WriteJSON(executeResultMsg))
|
|
})
|
|
defer server.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/api/kernels/test-kernel-id/channels"
|
|
executor := NewExecutor(wsURL, nil)
|
|
require.NoError(t, executor.Connect())
|
|
defer executor.Disconnect()
|
|
|
|
resultChan := make(chan *ExecutionResult, 10)
|
|
require.NoError(t, executor.ExecuteCodeStream("print('late result')", resultChan))
|
|
|
|
start := time.Now()
|
|
var gotLateResult bool
|
|
for result := range resultChan {
|
|
if result != nil && result.ExecutionCount == 1 {
|
|
gotLateResult = true
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
require.True(t, gotLateResult, "expected late execute_result to be delivered before stream close")
|
|
require.Less(t, elapsed, 100*time.Millisecond, "expected stream to close promptly after late execute_result")
|
|
}
|
|
|
|
func TestExecuteCodeStreamFallsBackWhenPollIntervalIsNonPositive(t *testing.T) {
|
|
previousPollInterval := execdflag.JupyterIdlePollInterval
|
|
execdflag.JupyterIdlePollInterval = 0
|
|
t.Cleanup(func() {
|
|
execdflag.JupyterIdlePollInterval = previousPollInterval
|
|
})
|
|
|
|
server := createTestServer(t, func(conn *websocket.Conn) {
|
|
var executeRequest Message
|
|
err := conn.ReadJSON(&executeRequest)
|
|
if err != nil {
|
|
t.Fatalf("failed to read execution request: %v", err)
|
|
}
|
|
|
|
statusContent, _ := json.Marshal(StatusUpdate{ExecutionState: StateIdle})
|
|
statusMsg := Message{
|
|
Header: Header{
|
|
MessageID: "status-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgStatus),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(statusContent),
|
|
}
|
|
require.NoError(t, conn.WriteJSON(statusMsg))
|
|
|
|
time.Sleep(15 * time.Millisecond)
|
|
|
|
resultContent, _ := json.Marshal(ExecuteResult{
|
|
ExecutionCount: 1,
|
|
Data: map[string]interface{}{
|
|
"text/plain": "Completed with fallback",
|
|
},
|
|
Metadata: map[string]interface{}{},
|
|
})
|
|
executeResultMsg := Message{
|
|
Header: Header{
|
|
MessageID: "result-msg-id",
|
|
Session: executeRequest.Header.Session,
|
|
MessageType: string(MsgExecuteResult),
|
|
},
|
|
ParentHeader: executeRequest.Header,
|
|
Content: json.RawMessage(resultContent),
|
|
}
|
|
require.NoError(t, conn.WriteJSON(executeResultMsg))
|
|
})
|
|
defer server.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/api/kernels/test-kernel-id/channels"
|
|
executor := NewExecutor(wsURL, nil)
|
|
require.NoError(t, executor.Connect())
|
|
defer executor.Disconnect()
|
|
|
|
resultChan := make(chan *ExecutionResult, 10)
|
|
require.NoError(t, executor.ExecuteCodeStream("print('fallback')", resultChan))
|
|
|
|
start := time.Now()
|
|
var gotLateResult bool
|
|
for result := range resultChan {
|
|
if result != nil && result.ExecutionCount == 1 {
|
|
gotLateResult = true
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
require.True(t, gotLateResult, "expected late execute_result to be delivered before stream close")
|
|
require.GreaterOrEqual(t, elapsed, 90*time.Millisecond, "expected non-positive poll interval to fall back to runtime default (100ms)")
|
|
require.Less(t, elapsed, 300*time.Millisecond, "expected fallback poll interval to still close stream promptly")
|
|
}
|