yao/sandbox/v2/box_attach_test.go
Max 2bd2a37c4b Implement VNC support in Sandbox V2 tests and enhance Docker test image
- Add new test cases for VNC URL generation and connection in `box_attach_test.go`, ensuring proper functionality of VNC features.
- Update the Docker test image to include VNC desktop components, enhancing the testing environment for graphical applications.
- Modify the entrypoint script to start a virtual framebuffer and VNC server, allowing for remote desktop access during tests.
- Increase the test timeout in the Makefile to accommodate longer-running VNC tests.

These changes improve the testing framework for Sandbox V2 by integrating VNC capabilities, facilitating better testing of graphical applications.
2026-03-05 15:07:53 +08:00

251 lines
5.6 KiB
Go

package sandbox_test
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
sandbox "github.com/yaoapp/yao/sandbox/v2"
)
func waitForPort(t *testing.T, box *sandbox.Box, port int, timeout time.Duration) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
url, err := box.Proxy(ctx, port, "/")
if err != nil {
t.Fatalf("Proxy URL: %v", err)
}
host := url[len("http://"):]
if i := len(host) - 1; host[i] == '/' {
host = host[:i]
}
for i := 0; i < len(host); i++ {
if host[i] == '/' {
host = host[:i]
break
}
}
deadline := time.After(timeout)
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-deadline:
t.Fatalf("port %d not ready within %v", port, timeout)
case <-ticker.C:
conn, err := net.DialTimeout("tcp", host, time.Second)
if err == nil {
conn.Close()
time.Sleep(200 * time.Millisecond)
return
}
fmt.Printf("waiting for %s: %v\n", host, err)
}
}
}
func TestAttachWS(t *testing.T) {
skipIfNoDocker(t)
img := testImage()
if img == "alpine:latest" {
t.Skip("WebSocket test requires sandbox-v2-test image with ws-echo service")
}
for _, pc := range testPools() {
t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, pc)
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
co.Ports = []sandbox.PortMapping{
{ContainerPort: 9800, HostPort: 0, Protocol: "tcp"},
}
})
waitForPort(t, box, 9800, 30*time.Second)
conn, err := box.Attach(t.Context(), 9800, sandbox.WithProtocol("ws"), sandbox.WithPath("/"))
if err != nil {
t.Fatalf("Attach WS: %v", err)
}
defer conn.Close()
if err := conn.Write([]byte("ping")); err != nil {
t.Fatalf("Write: %v", err)
}
msg, err := conn.Read()
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(msg) != "ping" {
t.Errorf("echo = %q, want %q", string(msg), "ping")
}
})
}
}
func TestAttachSSE(t *testing.T) {
skipIfNoDocker(t)
img := testImage()
if img == "alpine:latest" {
t.Skip("SSE test requires sandbox-v2-test image with sse-server service")
}
for _, pc := range testPools() {
t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, pc)
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
co.Ports = []sandbox.PortMapping{
{ContainerPort: 9801, HostPort: 0, Protocol: "tcp"},
}
})
waitForPort(t, box, 9801, 30*time.Second)
conn, err := box.Attach(t.Context(), 9801, sandbox.WithProtocol("sse"), sandbox.WithPath("/events"))
if err != nil {
t.Fatalf("Attach SSE: %v", err)
}
defer conn.Close()
count := 0
for event := range conn.Events {
if len(event) > 0 {
count++
}
if count >= 2 {
break
}
}
if count < 2 {
t.Errorf("received %d events, want >= 2", count)
}
})
}
}
func TestVNCURL(t *testing.T) {
skipIfNoDocker(t)
img := testImage()
if img == "alpine:latest" {
t.Skip("VNC test requires sandbox-v2-test image with VNC desktop")
}
for _, pc := range testPools() {
t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, pc)
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
co.VNC = true
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
url, err := box.VNC(ctx)
if err != nil {
t.Fatalf("VNC URL: %v", err)
}
if !strings.HasPrefix(url, "ws://") {
t.Fatalf("VNC URL = %q, want ws:// prefix", url)
}
t.Logf("VNC URL: %s", url)
})
}
}
func TestVNCConnect(t *testing.T) {
skipIfNoDocker(t)
img := testImage()
if img == "alpine:latest" {
t.Skip("VNC test requires sandbox-v2-test image with VNC desktop")
}
for _, pc := range testPools() {
t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, pc)
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
co.VNC = true
})
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
vncURL, err := box.VNC(ctx)
if err != nil {
t.Fatalf("VNC URL: %v", err)
}
t.Logf("VNC URL: %s", vncURL)
waitForWSEndpoint(t, vncURL, 30*time.Second)
dialer := websocket.Dialer{
Subprotocols: []string{"binary"},
HandshakeTimeout: 10 * time.Second,
}
ws, resp, err := dialer.DialContext(ctx, vncURL, http.Header{})
if err != nil {
extra := ""
if resp != nil {
extra = fmt.Sprintf(" (status %d)", resp.StatusCode)
}
t.Fatalf("VNC dial: %v%s", err, extra)
}
defer ws.Close()
ws.SetReadDeadline(time.Now().Add(10 * time.Second))
_, msg, err := ws.ReadMessage()
if err != nil {
t.Fatalf("VNC read: %v", err)
}
if !strings.HasPrefix(string(msg), "RFB ") {
t.Fatalf("VNC banner = %q, want RFB prefix", string(msg))
}
t.Logf("VNC banner: %s", strings.TrimSpace(string(msg)))
})
}
}
func waitForWSEndpoint(t *testing.T, wsURL string, timeout time.Duration) {
t.Helper()
httpURL := "http" + strings.TrimPrefix(wsURL, "ws")
if idx := strings.LastIndex(httpURL, "/ws"); idx > 0 {
httpURL = httpURL[:idx]
}
host := strings.TrimPrefix(httpURL, "http://")
if i := strings.Index(host, "/"); i > 0 {
host = host[:i]
}
deadline := time.After(timeout)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-deadline:
t.Fatalf("VNC endpoint %s not ready within %v", host, timeout)
case <-ticker.C:
conn, err := net.DialTimeout("tcp", host, time.Second)
if err == nil {
conn.Close()
time.Sleep(500 * time.Millisecond)
return
}
}
}
}