yao/tai/runtime/runtime_test.go
Max 7cccf62841 feat(tai): refactor Dial* functions and remove requireKubeConfig hard-fail
- Consolidate DialRemote/DialTunnel common logic into buildResources + dialEnv interface
- Remove strict capability check that prevented ConnResources creation for host-exec-only nodes
- Merge gRPC-discovered capabilities with registration-declared capabilities in DialTunnel
- Replace requireKubeConfig hard-fail with graceful skip when kubeconfig is absent
- Introduce tai/types package for shared Ports/Capabilities/SystemInfo/AuthInfo/NodeMeta
- Add tai/conn.go (ConnResources) and tai/dial.go (DialRemote/DialTunnel/DialLocal)
- Rename tai/sandbox → tai/runtime for clarity
- Update sandbox/v2, workspace, agent/sandbox/v2 test utilities for build-tag isolation

Made-with: Cursor
2026-03-12 15:44:24 +08:00

958 lines
21 KiB
Go

package runtime
import (
"context"
"io"
"os"
"strings"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
)
func taiTestDocker() string {
if addr := os.Getenv("TAI_TEST_DOCKER"); addr != "" {
return addr
}
return "tcp://127.0.0.1:2375"
}
func taiTestK8sHost() string { return os.Getenv("TAI_TEST_K8S_HOST") }
func taiTestK8sPort() string { return os.Getenv("TAI_TEST_K8S_PORT") }
func taiTestKubeConfig() string { return os.Getenv("TAI_TEST_KUBECONFIG") }
func TestHelpers(t *testing.T) {
t.Run("envSlice", func(t *testing.T) {
if got := envSlice(nil); got != nil {
t.Errorf("envSlice(nil) = %v", got)
}
s := envSlice(map[string]string{"A": "1", "B": "2"})
if len(s) != 2 {
t.Errorf("len = %d, want 2", len(s))
}
})
t.Run("proto", func(t *testing.T) {
if got := proto(""); got != "tcp" {
t.Errorf("proto empty = %q", got)
}
if got := proto("udp"); got != "udp" {
t.Errorf("proto udp = %q", got)
}
})
t.Run("hostIP", func(t *testing.T) {
if got := hostIP(""); got != "127.0.0.1" {
t.Errorf("hostIP empty = %q", got)
}
if got := hostIP("10.0.0.1"); got != "10.0.0.1" {
t.Errorf("hostIP explicit = %q", got)
}
})
}
func TestLocalRuntime(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
var containerID string
t.Run("Create", func(t *testing.T) {
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-sdk-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if id == "" {
t.Fatal("expected non-empty ID")
}
containerID = id
})
t.Run("Start", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
if err := sb.Start(ctx, containerID); err != nil {
t.Fatalf("Start: %v", err)
}
})
t.Run("Inspect", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
info, err := sb.Inspect(ctx, containerID)
if err != nil {
t.Fatalf("Inspect: %v", err)
}
if info.Status != "running" {
t.Errorf("status = %q, want running", info.Status)
}
if info.Image != "alpine:latest" {
t.Errorf("image = %q", info.Image)
}
})
t.Run("Exec", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
result, err := sb.Exec(ctx, containerID, []string{"echo", "hello"}, ExecOptions{})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("exitCode = %d", result.ExitCode)
}
if result.Stdout != "hello\n" {
t.Errorf("stdout = %q, want %q", result.Stdout, "hello\n")
}
})
t.Run("List", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
containers, err := sb.List(ctx, ListOptions{All: true})
if err != nil {
t.Fatalf("List: %v", err)
}
found := false
for _, c := range containers {
if c.ID == containerID {
found = true
break
}
}
if !found {
t.Error("container not found in list")
}
})
t.Run("Stop", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
if err := sb.Stop(ctx, containerID, 5*time.Second); err != nil {
t.Fatalf("Stop: %v", err)
}
})
t.Run("Remove", func(t *testing.T) {
if containerID == "" {
t.Skip("no container")
}
if err := sb.Remove(ctx, containerID, true); err != nil {
t.Fatalf("Remove: %v", err)
}
})
}
func TestLocalCreateWithPorts(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-sdk-port-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "5"},
Memory: 64 * 1024 * 1024,
CPUs: 0.5,
Ports: []PortMapping{
{ContainerPort: 8080, HostPort: 0, Protocol: "tcp"},
},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
info, err := sb.Inspect(ctx, id)
if err != nil {
t.Fatalf("Inspect: %v", err)
}
found := false
for _, p := range info.Ports {
if p.ContainerPort == 8080 {
found = true
if p.HostPort == 0 {
t.Error("HostPort should be resolved")
}
}
}
if !found {
t.Error("port 8080 not in Ports")
}
}
func TestLocalCreateWithVNC(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-sdk-vnc-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "5"},
Memory: 512 * 1024 * 1024,
VNC: true,
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
}
func TestLocalCreateWithEnvAndWorkDir(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-sdk-env-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "5"},
WorkingDir: "/tmp",
Env: map[string]string{"FOO": "bar"},
Binds: []string{},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
result, err := sb.Exec(ctx, id, []string{"printenv", "FOO"}, ExecOptions{WorkDir: "/tmp"})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.Stdout != "bar\n" {
t.Errorf("FOO = %q, want %q", result.Stdout, "bar\n")
}
}
func TestDockerRuntimeViaTai(t *testing.T) {
addr := taiTestDocker()
sb, err := NewDocker(addr)
if err != nil {
t.Skipf("Tai Docker proxy not available at %s: %v", addr, err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-docker-proxy-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "10"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
info, err := sb.Inspect(ctx, id)
if err != nil {
t.Fatalf("Inspect: %v", err)
}
if info.Status != "running" {
t.Errorf("status = %q", info.Status)
}
result, err := sb.Exec(ctx, id, []string{"echo", "via-tai"}, ExecOptions{})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.Stdout != "via-tai\n" {
t.Errorf("stdout = %q", result.Stdout)
}
containers, err := sb.List(ctx, ListOptions{All: true})
if err != nil {
t.Fatalf("List: %v", err)
}
found := false
for _, c := range containers {
if c.ID == id {
found = true
}
}
if !found {
t.Error("container not in list")
}
if err := sb.Stop(ctx, id, 5*time.Second); err != nil {
t.Fatalf("Stop: %v", err)
}
}
func TestListWithLabels(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
// List with non-matching labels should return empty
result, err := sb.List(context.Background(), ListOptions{
Labels: map[string]string{"tai-test-nonexist": "true"},
})
if err != nil {
t.Fatalf("List: %v", err)
}
if len(result) != 0 {
t.Errorf("expected 0, got %d", len(result))
}
}
func TestNewLocalInvalidAddr(t *testing.T) {
_, err := NewLocal("tcp://192.168.254.254:1")
if err == nil {
t.Error("expected error for unreachable Docker")
}
}
func TestPortStr(t *testing.T) {
if got := portStr(0); got != "" {
t.Errorf("portStr(0) = %q", got)
}
if got := portStr(8080); got != "8080" {
t.Errorf("portStr(8080) = %q", got)
}
}
func TestK8sRuntime(t *testing.T) {
host := taiTestK8sHost()
port := taiTestK8sPort()
kubeconfig := taiTestKubeConfig()
if host == "" || port == "" || kubeconfig == "" {
t.Skip("TAI_TEST_K8S_HOST, TAI_TEST_K8S_PORT, or TAI_TEST_KUBECONFIG not set")
}
addr := host + ":" + port
sb, err := NewK8s(addr, K8sOption{
Namespace: "default",
KubeConfig: kubeconfig,
})
if err != nil {
t.Skipf("K8s not available at %s: %v", addr, err)
}
defer sb.Close()
ctx := context.Background()
var podName string
t.Run("Create", func(t *testing.T) {
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-k8s-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "60"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if id == "" {
t.Fatal("expected non-empty name")
}
podName = id
})
t.Run("Start", func(t *testing.T) {
if podName == "" {
t.Skip("no pod")
}
if err := sb.Start(ctx, podName); err != nil {
t.Fatalf("Start (wait for Running): %v", err)
}
})
t.Run("Inspect", func(t *testing.T) {
if podName == "" {
t.Skip("no pod")
}
info, err := sb.Inspect(ctx, podName)
if err != nil {
t.Fatalf("Inspect: %v", err)
}
if info.Status != "Running" {
t.Errorf("status = %q, want Running", info.Status)
}
if info.Image != "alpine:latest" {
t.Errorf("image = %q", info.Image)
}
})
t.Run("Exec", func(t *testing.T) {
if podName == "" {
t.Skip("no pod")
}
result, err := sb.Exec(ctx, podName, []string{"echo", "k8s-hello"}, ExecOptions{})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("exitCode = %d", result.ExitCode)
}
if result.Stdout != "k8s-hello\n" {
t.Errorf("stdout = %q", result.Stdout)
}
})
t.Run("List", func(t *testing.T) {
if podName == "" {
t.Skip("no pod")
}
pods, err := sb.List(ctx, ListOptions{})
if err != nil {
t.Fatalf("List: %v", err)
}
found := false
for _, p := range pods {
if p.Name == podName {
found = true
break
}
}
if !found {
t.Error("pod not found in list")
}
})
t.Run("Remove", func(t *testing.T) {
if podName == "" {
t.Skip("no pod")
}
if err := sb.Remove(ctx, podName, true); err != nil {
t.Fatalf("Remove: %v", err)
}
})
}
func TestNewK8sMissingKubeConfig(t *testing.T) {
_, err := NewK8s("127.0.0.1:6443")
if err == nil {
t.Error("expected error for missing kubeconfig")
}
}
func TestNewK8sBadKubeConfig(t *testing.T) {
_, err := NewK8s("127.0.0.1:6443", K8sOption{KubeConfig: "/nonexistent/kubeconfig.yml"})
if err == nil {
t.Error("expected error for bad kubeconfig path")
}
}
func TestK8sBuildResources(t *testing.T) {
r := buildResources(512*1024*1024, 1.5)
mem := r.Limits[corev1.ResourceMemory]
if mem.Value() != 512*1024*1024 {
t.Errorf("memory = %d, want %d", mem.Value(), 512*1024*1024)
}
cpu := r.Limits[corev1.ResourceCPU]
if cpu.MilliValue() != 1500 {
t.Errorf("cpu = %dm, want 1500m", cpu.MilliValue())
}
}
func TestK8sBuildResourcesPartial(t *testing.T) {
r := buildResources(0, 0.5)
if _, ok := r.Limits[corev1.ResourceMemory]; ok {
t.Error("memory should not be set when 0")
}
cpu := r.Limits[corev1.ResourceCPU]
if cpu.MilliValue() != 500 {
t.Errorf("cpu = %dm, want 500m", cpu.MilliValue())
}
}
func TestK8sRuntimeStopAndRemove(t *testing.T) {
host := taiTestK8sHost()
port := taiTestK8sPort()
kubeconfig := taiTestKubeConfig()
if host == "" || port == "" || kubeconfig == "" {
t.Skip("TAI_TEST_K8S_HOST, TAI_TEST_K8S_PORT, or TAI_TEST_KUBECONFIG not set")
}
addr := host + ":" + port
sb, err := NewK8s(addr, K8sOption{
Namespace: "default",
KubeConfig: kubeconfig,
})
if err != nil {
t.Skipf("K8s not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-k8s-stop-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "60"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
if err := sb.Stop(ctx, id, 5*time.Second); err != nil {
t.Fatalf("Stop: %v", err)
}
// Remove should succeed even if already deleted by Stop
if err := sb.Remove(ctx, id, true); err != nil {
t.Logf("Remove after Stop: %v (expected if already deleted)", err)
}
}
func TestK8sCreateWithResources(t *testing.T) {
host := taiTestK8sHost()
port := taiTestK8sPort()
kubeconfig := taiTestKubeConfig()
if host == "" || port == "" || kubeconfig == "" {
t.Skip("TAI_TEST_K8S_HOST, TAI_TEST_K8S_PORT, or TAI_TEST_KUBECONFIG not set")
}
addr := host + ":" + port
sb, err := NewK8s(addr, K8sOption{
Namespace: "default",
KubeConfig: kubeconfig,
})
if err != nil {
t.Skipf("K8s not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-k8s-res-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "10"},
Memory: 64 * 1024 * 1024,
CPUs: 0.5,
Env: map[string]string{"FOO": "bar"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
// Exec with WorkDir and Env
result, err := sb.Exec(ctx, id, []string{"echo", "hi"}, ExecOptions{
WorkDir: "/tmp",
Env: map[string]string{"BAR": "baz"},
})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("exitCode = %d", result.ExitCode)
}
}
func TestK8sRemoveNonExistent(t *testing.T) {
host := taiTestK8sHost()
port := taiTestK8sPort()
kubeconfig := taiTestKubeConfig()
if host == "" || port == "" || kubeconfig == "" {
t.Skip("TAI_TEST_K8S_HOST, TAI_TEST_K8S_PORT, or TAI_TEST_KUBECONFIG not set")
}
addr := host + ":" + port
sb, err := NewK8s(addr, K8sOption{
Namespace: "default",
KubeConfig: kubeconfig,
})
if err != nil {
t.Skipf("K8s not available: %v", err)
}
defer sb.Close()
// Remove non-existent should not error
err = sb.Remove(context.Background(), "nonexistent-pod-12345", false)
if err != nil {
t.Errorf("Remove non-existent should return nil, got: %v", err)
}
}
func TestNewK8sRelativeKubeConfig(t *testing.T) {
kubeconfig := taiTestKubeConfig()
if kubeconfig == "" {
t.Skip("TAI_TEST_KUBECONFIG not set")
}
// NewK8s with empty addr should still work (uses kubeconfig's server)
_, err := NewK8s("", K8sOption{
KubeConfig: kubeconfig,
})
if err != nil {
t.Skipf("K8s not available: %v", err)
}
}
func TestCreateWithLabels(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
labels := map[string]string{
"sandbox-id": "test-123",
"sandbox-owner": "user1",
}
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-label-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "10"},
Labels: labels,
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
info, err := sb.Inspect(ctx, id)
if err != nil {
t.Fatalf("Inspect: %v", err)
}
for k, v := range labels {
if info.Labels[k] != v {
t.Errorf("label %q = %q, want %q", k, info.Labels[k], v)
}
}
listed, err := sb.List(ctx, ListOptions{
Labels: map[string]string{"sandbox-id": "test-123"},
})
if err != nil {
t.Fatalf("List: %v", err)
}
found := false
for _, c := range listed {
if c.ID == id {
found = true
if c.Labels["sandbox-owner"] != "user1" {
t.Errorf("list labels missing sandbox-owner")
}
}
}
if !found {
t.Error("labeled container not found in filtered list")
}
}
func TestCreateWithUser(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-user-test",
Image: "alpine:latest",
Cmd: []string{"sleep", "10"},
User: "1000:1000",
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
result, err := sb.Exec(ctx, id, []string{"id", "-u"}, ExecOptions{})
if err != nil {
t.Fatalf("Exec: %v", err)
}
if result.Stdout != "1000\n" {
t.Errorf("user id = %q, want %q", result.Stdout, "1000\n")
}
}
func TestExecStream_ShortCommand(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-stream-short",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
stream, err := sb.ExecStream(ctx, id, []string{"echo", "hello-stream"}, ExecOptions{})
if err != nil {
t.Fatalf("ExecStream: %v", err)
}
out, err := io.ReadAll(stream.Stdout)
if err != nil {
t.Fatalf("ReadAll stdout: %v", err)
}
if string(out) != "hello-stream\n" {
t.Errorf("stdout = %q, want %q", string(out), "hello-stream\n")
}
code, err := stream.Wait()
if err != nil {
t.Fatalf("Wait: %v", err)
}
if code != 0 {
t.Errorf("exit code = %d, want 0", code)
}
}
func TestExecStream_Stdin(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-stream-stdin",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
stream, err := sb.ExecStream(ctx, id, []string{"cat"}, ExecOptions{})
if err != nil {
t.Fatalf("ExecStream: %v", err)
}
_, err = stream.Stdin.Write([]byte("from-stdin\n"))
if err != nil {
t.Fatalf("Write stdin: %v", err)
}
stream.Stdin.Close()
out, err := io.ReadAll(stream.Stdout)
if err != nil {
t.Fatalf("ReadAll stdout: %v", err)
}
if string(out) != "from-stdin\n" {
t.Errorf("stdout = %q, want %q", string(out), "from-stdin\n")
}
code, err := stream.Wait()
if err != nil {
t.Fatalf("Wait: %v", err)
}
if code != 0 {
t.Errorf("exit code = %d, want 0", code)
}
}
func TestExecStream_ExitCode(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-stream-exit",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
stream, err := sb.ExecStream(ctx, id, []string{"sh", "-c", "exit 42"}, ExecOptions{})
if err != nil {
t.Fatalf("ExecStream: %v", err)
}
io.ReadAll(stream.Stdout)
code, err := stream.Wait()
if err != nil {
t.Fatalf("Wait: %v", err)
}
if code != 42 {
t.Errorf("exit code = %d, want 42", code)
}
}
func TestExecStream_Stderr(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-stream-stderr",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
stream, err := sb.ExecStream(ctx, id, []string{"sh", "-c", "echo err-msg >&2"}, ExecOptions{})
if err != nil {
t.Fatalf("ExecStream: %v", err)
}
stderr, err := io.ReadAll(stream.Stderr)
if err != nil {
t.Fatalf("ReadAll stderr: %v", err)
}
if !strings.Contains(string(stderr), "err-msg") {
t.Errorf("stderr = %q, want to contain %q", string(stderr), "err-msg")
}
code, _ := stream.Wait()
if code != 0 {
t.Errorf("exit code = %d, want 0", code)
}
}
func TestExecStream_Cancel(t *testing.T) {
sb, err := NewLocal("")
if err != nil {
t.Skipf("Docker not available: %v", err)
}
defer sb.Close()
ctx := context.Background()
id, err := sb.Create(ctx, CreateOptions{
Name: "tai-stream-cancel",
Image: "alpine:latest",
Cmd: []string{"sleep", "30"},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer sb.Remove(ctx, id, true)
if err := sb.Start(ctx, id); err != nil {
t.Fatalf("Start: %v", err)
}
stream, err := sb.ExecStream(ctx, id, []string{"sleep", "300"}, ExecOptions{})
if err != nil {
t.Fatalf("ExecStream: %v", err)
}
stream.Cancel()
done := make(chan struct{})
go func() {
stream.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Error("Wait did not return after Cancel within 5s")
}
}
func TestParseUID(t *testing.T) {
tests := []struct {
input string
want int64
ok bool
}{
{"1000", 1000, true},
{"1000:1000", 1000, true},
{"0", 0, true},
{"abc", 0, false},
}
for _, tt := range tests {
got, err := parseUID(tt.input)
if tt.ok && err != nil {
t.Errorf("parseUID(%q): unexpected error %v", tt.input, err)
}
if !tt.ok && err == nil {
t.Errorf("parseUID(%q): expected error", tt.input)
}
if tt.ok && got != tt.want {
t.Errorf("parseUID(%q) = %d, want %d", tt.input, got, tt.want)
}
}
}