github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/integration_test.go (about) 1 //go:build integration 2 3 package sandbox 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "os" 10 "strconv" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 ) 16 17 // testClient 从环境变量创建集成测试用的客户端。 18 func testClient(t *testing.T) *Client { 19 t.Helper() 20 21 apiKey := os.Getenv("QINIU_API_KEY") 22 apiURL := os.Getenv("QINIU_SANDBOX_API_URL") 23 if apiKey == "" { 24 t.Fatal("需要设置 QINIU_API_KEY 环境变量") 25 } 26 27 cfg := &Config{ 28 APIKey: apiKey, 29 Endpoint: apiURL, 30 } 31 if v := strings.TrimSpace(os.Getenv("SANDBOX_RETRY_MAX")); v != "" { 32 n, err := strconv.Atoi(v) 33 if err != nil || n < 0 { 34 t.Fatalf("SANDBOX_RETRY_MAX 必须是有效的非负整数,当前值:%q", v) 35 } 36 cfg.RetryMax = &n 37 } 38 c, err := NewClient(cfg) 39 if err != nil { 40 t.Fatalf("创建客户端失败: %v", err) 41 } 42 return c 43 } 44 45 func TestIntegrationListTemplates(t *testing.T) { 46 c := testClient(t) 47 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 48 defer cancel() 49 50 templates, err := c.ListTemplates(ctx, nil) 51 if err != nil { 52 t.Fatalf("ListTemplates 失败: %v", err) 53 } 54 t.Logf("共 %d 个模板", len(templates)) 55 for _, tmpl := range templates { 56 t.Logf(" - %s (buildStatus=%s)", tmpl.TemplateID, tmpl.BuildStatus) 57 } 58 } 59 60 func TestIntegrationListDefaultTemplates(t *testing.T) { 61 c := testClient(t) 62 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 63 defer cancel() 64 65 templates, err := c.ListDefaultTemplates(ctx) 66 if err != nil { 67 t.Fatalf("ListDefaultTemplates 失败: %v", err) 68 } 69 t.Logf("共 %d 个默认模板", len(templates)) 70 for _, tmpl := range templates { 71 t.Logf(" - %s (buildStatus=%s)", tmpl.TemplateID, tmpl.BuildStatus) 72 } 73 } 74 75 func TestIntegrationListSandboxes(t *testing.T) { 76 c := testClient(t) 77 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 78 defer cancel() 79 80 sandboxes, err := c.List(ctx, nil) 81 if err != nil { 82 t.Fatalf("List 失败: %v", err) 83 } 84 t.Logf("共 %d 个沙箱", len(sandboxes)) 85 for _, sb := range sandboxes { 86 t.Logf(" - %s (template=%s)", sb.SandboxID, sb.TemplateID) 87 } 88 } 89 90 func TestIntegrationSandboxLifecycle(t *testing.T) { 91 c := testClient(t) 92 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 93 defer cancel() 94 95 // 1. 获取可用模板 96 templates, err := c.ListTemplates(ctx, nil) 97 if err != nil { 98 t.Fatalf("ListTemplates 失败: %v", err) 99 } 100 101 var templateID string 102 for _, tmpl := range templates { 103 if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded { 104 templateID = tmpl.TemplateID 105 break 106 } 107 } 108 if templateID == "" { 109 t.Skip("没有可用模板,跳过生命周期测试") 110 } 111 t.Logf("使用模板: %s", templateID) 112 113 // 2. 创建沙箱并等待就绪 114 timeout := int32(60) 115 sb, info, err := c.CreateAndWait(ctx, CreateParams{ 116 TemplateID: templateID, 117 Timeout: &timeout, 118 }, WithPollInterval(2*time.Second)) 119 if err != nil { 120 t.Fatalf("CreateAndWait 失败: %v", err) 121 } 122 t.Logf("沙箱已创建: %s (state=%s)", sb.ID(), info.State) 123 124 // 确保测试结束时清理沙箱 125 killed := false 126 defer func() { 127 if killed { 128 return 129 } 130 killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second) 131 defer killCancel() 132 if err := sb.Kill(killCtx); err != nil { 133 t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err) 134 } else { 135 t.Logf("沙箱 %s 已清理", sb.ID()) 136 } 137 }() 138 139 // 3. 检查运行状态 140 running, err := sb.IsRunning(ctx) 141 if err != nil { 142 t.Fatalf("IsRunning 失败: %v", err) 143 } 144 if !running { 145 t.Fatal("沙箱应处于运行状态") 146 } 147 148 // 4. 获取详细信息 149 detail, err := sb.GetInfo(ctx) 150 if err != nil { 151 t.Fatalf("GetInfo 失败: %v", err) 152 } 153 t.Logf("沙箱详情: state=%s, templateID=%s, cpuCount=%d, memoryMB=%d", 154 detail.State, detail.TemplateID, detail.CPUCount, detail.MemoryMB) 155 156 // 5. 更新超时时间 157 if err := sb.SetTimeout(ctx, 120*time.Second); err != nil { 158 t.Fatalf("SetTimeout 失败: %v", err) 159 } 160 t.Log("超时时间已更新为 120s") 161 162 // 6. 在沙箱列表中确认可见 163 sandboxes, err := c.List(ctx, nil) 164 if err != nil { 165 t.Fatalf("List 失败: %v", err) 166 } 167 found := false 168 for _, s := range sandboxes { 169 if s.SandboxID == sb.ID() { 170 found = true 171 break 172 } 173 } 174 if !found { 175 t.Fatal("新创建的沙箱未出现在列表中") 176 } 177 178 // 7. 终止沙箱 179 if err := sb.Kill(ctx); err != nil { 180 t.Fatalf("Kill 失败: %v", err) 181 } 182 killed = true 183 t.Log("沙箱已终止") 184 } 185 186 // createTestSandbox 创建一个用于 envd 集成测试的沙箱并等待就绪。 187 func createTestSandbox(t *testing.T, c *Client, ctx context.Context) *Sandbox { 188 t.Helper() 189 190 templates, err := c.ListTemplates(ctx, nil) 191 if err != nil { 192 t.Fatalf("ListTemplates 失败: %v", err) 193 } 194 195 var templateID string 196 for _, tmpl := range templates { 197 if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded { 198 templateID = tmpl.TemplateID 199 break 200 } 201 } 202 if templateID == "" { 203 t.Skip("没有可用模板,跳过测试") 204 } 205 206 timeout := int32(120) 207 sb, _, err := c.CreateAndWait(ctx, CreateParams{ 208 TemplateID: templateID, 209 Timeout: &timeout, 210 }, WithPollInterval(2*time.Second)) 211 if err != nil { 212 t.Fatalf("CreateAndWait 失败: %v", err) 213 } 214 215 t.Cleanup(func() { 216 killCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 217 defer cancel() 218 if err := sb.Kill(killCtx); err != nil { 219 t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err) 220 } 221 }) 222 223 return sb 224 } 225 226 func TestIntegrationFilesWriteRead(t *testing.T) { 227 c := testClient(t) 228 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 229 defer cancel() 230 231 sb := createTestSandbox(t, c, ctx) 232 t.Logf("沙箱: %s", sb.ID()) 233 234 // 写入文件 235 content := []byte("hello sandbox\n") 236 _, err := sb.Files().Write(ctx, "/tmp/test-file.txt", content) 237 if err != nil { 238 t.Fatalf("Write 失败: %v", err) 239 } 240 t.Log("文件写入成功") 241 242 // 读取文件 243 got, err := sb.Files().Read(ctx, "/tmp/test-file.txt") 244 if err != nil { 245 t.Fatalf("Read 失败: %v", err) 246 } 247 if string(got) != string(content) { 248 t.Fatalf("文件内容不匹配: got %q, want %q", string(got), string(content)) 249 } 250 t.Log("文件读取内容一致") 251 } 252 253 func TestIntegrationCommandsRun(t *testing.T) { 254 c := testClient(t) 255 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 256 defer cancel() 257 258 sb := createTestSandbox(t, c, ctx) 259 t.Logf("沙箱: %s", sb.ID()) 260 261 // 执行简单命令 262 result, err := sb.Commands().Run(ctx, "echo hello world") 263 if err != nil { 264 t.Fatalf("Run 失败: %v", err) 265 } 266 t.Logf("命令结果: exitCode=%d, stdout=%q, stderr=%q", result.ExitCode, result.Stdout, result.Stderr) 267 if result.ExitCode != 0 { 268 t.Fatalf("命令退出码 %d,期望 0", result.ExitCode) 269 } 270 if result.Stdout != "hello world\n" { 271 t.Fatalf("stdout = %q, want %q", result.Stdout, "hello world\n") 272 } 273 } 274 275 func TestIntegrationGetHost(t *testing.T) { 276 c := testClient(t) 277 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 278 defer cancel() 279 280 sb := createTestSandbox(t, c, ctx) 281 282 host := sb.GetHost(8080) 283 if host == "" { 284 t.Fatal("GetHost 返回空字符串") 285 } 286 t.Logf("GetHost(8080) = %s", host) 287 288 // 验证格式: {port}-{sandboxID}.{domain} 289 expected := "8080-" + sb.ID() 290 if len(host) < len(expected) || host[:len(expected)] != expected { 291 t.Fatalf("GetHost 格式不符: got %q, want prefix %q", host, expected) 292 } 293 } 294 295 func TestIntegrationFilesystemOperations(t *testing.T) { 296 c := testClient(t) 297 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 298 defer cancel() 299 300 sb := createTestSandbox(t, c, ctx) 301 t.Logf("沙箱: %s", sb.ID()) 302 303 fs := sb.Files() 304 305 // 创建目录 306 dirInfo, err := fs.MakeDir(ctx, "/tmp/test-dir") 307 if err != nil { 308 t.Fatalf("MakeDir 失败: %v", err) 309 } 310 t.Logf("创建目录: %s (type=%s)", dirInfo.Path, dirInfo.Type) 311 312 // 写入文件 313 _, err = fs.Write(ctx, "/tmp/test-dir/hello.txt", []byte("hello")) 314 if err != nil { 315 t.Fatalf("Write 失败: %v", err) 316 } 317 318 // 列目录 319 entries, err := fs.List(ctx, "/tmp/test-dir") 320 if err != nil { 321 t.Fatalf("List 失败: %v", err) 322 } 323 t.Logf("目录内容: %d 项", len(entries)) 324 325 // 文件存在 326 exists, err := fs.Exists(ctx, "/tmp/test-dir/hello.txt") 327 if err != nil { 328 t.Fatalf("Exists 失败: %v", err) 329 } 330 if !exists { 331 t.Fatal("文件应该存在") 332 } 333 334 // 获取文件信息 335 info, err := fs.GetInfo(ctx, "/tmp/test-dir/hello.txt") 336 if err != nil { 337 t.Fatalf("GetInfo 失败: %v", err) 338 } 339 t.Logf("文件信息: name=%s, size=%d, type=%s", info.Name, info.Size, info.Type) 340 341 // 重命名 342 newInfo, err := fs.Rename(ctx, "/tmp/test-dir/hello.txt", "/tmp/test-dir/renamed.txt") 343 if err != nil { 344 t.Fatalf("Rename 失败: %v", err) 345 } 346 t.Logf("重命名: %s -> %s", "/tmp/test-dir/hello.txt", newInfo.Path) 347 348 // 删除 349 if err := fs.Remove(ctx, "/tmp/test-dir/renamed.txt"); err != nil { 350 t.Fatalf("Remove 失败: %v", err) 351 } 352 t.Log("文件已删除") 353 354 // 验证文件不存在 355 exists, err = fs.Exists(ctx, "/tmp/test-dir/renamed.txt") 356 if err != nil { 357 t.Fatalf("Exists 失败: %v", err) 358 } 359 if exists { 360 t.Fatal("文件应已不存在") 361 } 362 } 363 364 func TestIntegrationUploadDownload(t *testing.T) { 365 c := testClient(t) 366 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 367 defer cancel() 368 369 sb := createTestSandbox(t, c, ctx) 370 t.Logf("沙箱: %s", sb.ID()) 371 372 // 通过 Files().Write() 写入文件 373 content := []byte("upload test content\n") 374 _, err := sb.Files().Write(ctx, "/tmp/uploaded.txt", content) 375 if err != nil { 376 t.Fatalf("Files().Write 失败: %v", err) 377 } 378 t.Log("文件写入成功") 379 380 // 通过 Files().Read() 读取文件 381 got, err := sb.Files().Read(ctx, "/tmp/uploaded.txt") 382 if err != nil { 383 t.Fatalf("Files().Read 失败: %v", err) 384 } 385 if string(got) != string(content) { 386 t.Fatalf("文件内容不匹配: got %q, want %q", string(got), string(content)) 387 } 388 t.Log("文件读取内容一致") 389 } 390 391 func TestIntegrationWriteFiles(t *testing.T) { 392 c := testClient(t) 393 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 394 defer cancel() 395 396 sb := createTestSandbox(t, c, ctx) 397 t.Logf("沙箱: %s", sb.ID()) 398 399 files := []WriteEntry{ 400 {Path: "/tmp/batch-1.txt", Data: []byte("content one")}, 401 {Path: "/tmp/batch-2.txt", Data: []byte("content two")}, 402 {Path: "/tmp/batch-3.txt", Data: []byte("content three")}, 403 } 404 405 infos, err := sb.Files().WriteFiles(ctx, files) 406 if err != nil { 407 t.Fatalf("WriteFiles 失败: %v", err) 408 } 409 if len(infos) != 3 { 410 t.Fatalf("WriteFiles 返回 %d 个结果,期望 3", len(infos)) 411 } 412 413 // 逐个读回验证内容 414 for i, f := range files { 415 got, err := sb.Files().Read(ctx, f.Path) 416 if err != nil { 417 t.Fatalf("Read %s 失败: %v", f.Path, err) 418 } 419 if string(got) != string(f.Data) { 420 t.Fatalf("文件 %s 内容不匹配: got %q, want %q", f.Path, string(got), string(f.Data)) 421 } 422 t.Logf("文件 %d (%s) 验证通过: name=%s, size=%d", i, f.Path, infos[i].Name, infos[i].Size) 423 } 424 } 425 426 func TestIntegrationReadText(t *testing.T) { 427 c := testClient(t) 428 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 429 defer cancel() 430 431 sb := createTestSandbox(t, c, ctx) 432 t.Logf("沙箱: %s", sb.ID()) 433 434 content := "hello read text\n" 435 _, err := sb.Files().Write(ctx, "/tmp/readtext.txt", []byte(content)) 436 if err != nil { 437 t.Fatalf("Write 失败: %v", err) 438 } 439 440 got, err := sb.Files().ReadText(ctx, "/tmp/readtext.txt") 441 if err != nil { 442 t.Fatalf("ReadText 失败: %v", err) 443 } 444 if got != content { 445 t.Fatalf("ReadText 内容不匹配: got %q, want %q", got, content) 446 } 447 t.Log("ReadText 验证通过") 448 } 449 450 func TestIntegrationReadStream(t *testing.T) { 451 c := testClient(t) 452 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 453 defer cancel() 454 455 sb := createTestSandbox(t, c, ctx) 456 t.Logf("沙箱: %s", sb.ID()) 457 458 content := []byte("hello read stream\n") 459 _, err := sb.Files().Write(ctx, "/tmp/readstream.txt", content) 460 if err != nil { 461 t.Fatalf("Write 失败: %v", err) 462 } 463 464 rc, err := sb.Files().ReadStream(ctx, "/tmp/readstream.txt") 465 if err != nil { 466 t.Fatalf("ReadStream 失败: %v", err) 467 } 468 defer rc.Close() 469 470 got, err := io.ReadAll(rc) 471 if err != nil { 472 t.Fatalf("读取流失败: %v", err) 473 } 474 if string(got) != string(content) { 475 t.Fatalf("ReadStream 内容不匹配: got %q, want %q", string(got), string(content)) 476 } 477 t.Log("ReadStream 验证通过") 478 } 479 480 // --- Commands 异步执行与进程管理 --- 481 482 func TestIntegrationCommandsStartWait(t *testing.T) { 483 c := testClient(t) 484 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 485 defer cancel() 486 487 sb := createTestSandbox(t, c, ctx) 488 t.Logf("沙箱: %s", sb.ID()) 489 490 handle, err := sb.Commands().Start(ctx, "sleep 1 && echo done") 491 if err != nil { 492 t.Fatalf("Start 失败: %v", err) 493 } 494 495 result, err := handle.Wait() 496 if err != nil { 497 t.Fatalf("Wait 失败: %v", err) 498 } 499 500 if handle.PID() == 0 { 501 t.Fatal("PID 应大于 0") 502 } 503 if result.ExitCode != 0 { 504 t.Fatalf("ExitCode = %d, want 0", result.ExitCode) 505 } 506 if !strings.Contains(result.Stdout, "done") { 507 t.Fatalf("Stdout = %q, 应包含 'done'", result.Stdout) 508 } 509 t.Logf("Start/Wait 验证通过: PID=%d, ExitCode=%d, Stdout=%q", handle.PID(), result.ExitCode, result.Stdout) 510 } 511 512 func TestIntegrationCommandsKill(t *testing.T) { 513 c := testClient(t) 514 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 515 defer cancel() 516 517 sb := createTestSandbox(t, c, ctx) 518 t.Logf("沙箱: %s", sb.ID()) 519 520 handle, err := sb.Commands().Start(ctx, "sleep 300") 521 if err != nil { 522 t.Fatalf("Start 失败: %v", err) 523 } 524 525 // 等待 PID 被设置 526 if _, err := handle.WaitPID(ctx); err != nil { 527 t.Fatalf("WaitPID 失败: %v", err) 528 } 529 t.Logf("进程已启动: PID=%d", handle.PID()) 530 531 // Kill 进程 532 if err := sb.Commands().Kill(ctx, handle.PID()); err != nil { 533 t.Fatalf("Kill 失败: %v", err) 534 } 535 536 result, err := handle.Wait() 537 if err != nil { 538 t.Fatalf("Wait 失败: %v", err) 539 } 540 541 if result.ExitCode == 0 { 542 t.Fatal("被 Kill 的进程 ExitCode 不应为 0") 543 } 544 t.Logf("Kill 验证通过: ExitCode=%d", result.ExitCode) 545 } 546 547 func TestIntegrationCommandsList(t *testing.T) { 548 c := testClient(t) 549 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 550 defer cancel() 551 552 sb := createTestSandbox(t, c, ctx) 553 t.Logf("沙箱: %s", sb.ID()) 554 555 handle, err := sb.Commands().Start(ctx, "sleep 300") 556 if err != nil { 557 t.Fatalf("Start 失败: %v", err) 558 } 559 560 // 等待 PID 被设置 561 if _, err := handle.WaitPID(ctx); err != nil { 562 t.Fatalf("WaitPID 失败: %v", err) 563 } 564 t.Logf("进程已启动: PID=%d", handle.PID()) 565 566 // 列出进程 567 infos, err := sb.Commands().List(ctx) 568 if err != nil { 569 t.Fatalf("List 失败: %v", err) 570 } 571 572 found := false 573 for _, info := range infos { 574 if info.PID == handle.PID() { 575 found = true 576 t.Logf("找到进程: PID=%d, Cmd=%s, Args=%v", info.PID, info.Cmd, info.Args) 577 break 578 } 579 } 580 if !found { 581 t.Fatalf("进程列表中未找到 PID=%d,共 %d 个进程", handle.PID(), len(infos)) 582 } 583 584 // 清理 585 _ = handle.Kill(ctx) 586 _, _ = handle.Wait() 587 t.Log("List 验证通过") 588 } 589 590 func TestIntegrationCommandsSendStdin(t *testing.T) { 591 c := testClient(t) 592 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 593 defer cancel() 594 595 sb := createTestSandbox(t, c, ctx) 596 t.Logf("沙箱: %s", sb.ID()) 597 598 // 启动 sleep 长命令(stdin 默认禁用,SendStdin 验证 RPC 调用不报错即可) 599 handle, err := sb.Commands().Start(ctx, "sleep 300") 600 if err != nil { 601 t.Fatalf("Start 失败: %v", err) 602 } 603 604 // 等待 PID 被设置 605 if _, err := handle.WaitPID(ctx); err != nil { 606 t.Fatalf("WaitPID 失败: %v", err) 607 } 608 609 // 发送 stdin(stdin 未启用,服务端会返回错误,验证错误信息符合预期) 610 err = sb.Commands().SendStdin(ctx, handle.PID(), []byte("hello\n")) 611 if err != nil { 612 // stdin 未启用时,服务端返回 "stdin not enabled" 错误是预期行为 613 if strings.Contains(err.Error(), "stdin not enabled") { 614 t.Logf("SendStdin 返回预期错误: %v", err) 615 } else { 616 t.Fatalf("SendStdin 失败(非预期错误): %v", err) 617 } 618 } else { 619 t.Log("SendStdin RPC 调用成功(数据可能被丢弃)") 620 } 621 622 // 清理 623 _ = handle.Kill(ctx) 624 _, _ = handle.Wait() 625 t.Log("SendStdin 验证通过") 626 } 627 628 func TestIntegrationCommandsWithCallbacks(t *testing.T) { 629 c := testClient(t) 630 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 631 defer cancel() 632 633 sb := createTestSandbox(t, c, ctx) 634 t.Logf("沙箱: %s", sb.ID()) 635 636 var ( 637 mu sync.Mutex 638 gotStdout []byte 639 gotStderr []byte 640 ) 641 642 result, err := sb.Commands().Run(ctx, "echo out && echo err >&2", 643 WithOnStdout(func(data []byte) { 644 mu.Lock() 645 defer mu.Unlock() 646 gotStdout = append(gotStdout, data...) 647 }), 648 WithOnStderr(func(data []byte) { 649 mu.Lock() 650 defer mu.Unlock() 651 gotStderr = append(gotStderr, data...) 652 }), 653 ) 654 if err != nil { 655 t.Fatalf("Run 失败: %v", err) 656 } 657 if result.ExitCode != 0 { 658 t.Fatalf("ExitCode = %d, want 0", result.ExitCode) 659 } 660 661 mu.Lock() 662 stdoutStr := string(gotStdout) 663 stderrStr := string(gotStderr) 664 mu.Unlock() 665 666 if !strings.Contains(stdoutStr, "out") { 667 t.Fatalf("Stdout 回调未收到预期数据: %q", stdoutStr) 668 } 669 if !strings.Contains(stderrStr, "err") { 670 t.Fatalf("Stderr 回调未收到预期数据: %q", stderrStr) 671 } 672 t.Logf("回调验证通过: stdout=%q, stderr=%q", stdoutStr, stderrStr) 673 } 674 675 func TestIntegrationCommandsWithOptions(t *testing.T) { 676 c := testClient(t) 677 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 678 defer cancel() 679 680 sb := createTestSandbox(t, c, ctx) 681 t.Logf("沙箱: %s", sb.ID()) 682 683 // 测试 WithCwd 684 result, err := sb.Commands().Run(ctx, "pwd", WithCwd("/tmp")) 685 if err != nil { 686 t.Fatalf("Run with WithCwd 失败: %v", err) 687 } 688 if !strings.Contains(result.Stdout, "/tmp") { 689 t.Fatalf("WithCwd: Stdout = %q, 应包含 '/tmp'", result.Stdout) 690 } 691 t.Logf("WithCwd 验证通过: Stdout=%q", result.Stdout) 692 693 // 测试 WithEnvs 694 result, err = sb.Commands().Run(ctx, "echo $FOO", WithEnvs(map[string]string{"FOO": "BAR"})) 695 if err != nil { 696 t.Fatalf("Run with WithEnvs 失败: %v", err) 697 } 698 if !strings.Contains(result.Stdout, "BAR") { 699 t.Fatalf("WithEnvs: Stdout = %q, 应包含 'BAR'", result.Stdout) 700 } 701 t.Logf("WithEnvs 验证通过: Stdout=%q", result.Stdout) 702 } 703 704 // --- PTY 交互 --- 705 706 func TestIntegrationPtyCreateAndKill(t *testing.T) { 707 c := testClient(t) 708 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 709 defer cancel() 710 711 sb := createTestSandbox(t, c, ctx) 712 t.Logf("沙箱: %s", sb.ID()) 713 714 var ( 715 mu sync.Mutex 716 output []byte 717 ) 718 719 handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24}, 720 WithOnStdout(func(data []byte) { 721 mu.Lock() 722 defer mu.Unlock() 723 output = append(output, data...) 724 }), 725 ) 726 if err != nil { 727 t.Fatalf("Pty.Create 失败: %v", err) 728 } 729 730 // 等待 PID 被设置 731 if _, err := handle.WaitPID(ctx); err != nil { 732 t.Fatalf("WaitPID 失败: %v", err) 733 } 734 t.Logf("PTY 已创建: PID=%d", handle.PID()) 735 736 // 等待一些 PTY 输出 737 time.Sleep(2 * time.Second) 738 739 // Kill PTY 740 if err := sb.Pty().Kill(ctx, handle.PID()); err != nil { 741 t.Fatalf("Pty.Kill 失败: %v", err) 742 } 743 744 result, err := handle.Wait() 745 if err != nil { 746 t.Fatalf("Wait 失败: %v", err) 747 } 748 749 mu.Lock() 750 outputStr := string(output) 751 mu.Unlock() 752 753 t.Logf("PTY 输出长度: %d bytes, ExitCode=%d", len(outputStr), result.ExitCode) 754 if len(outputStr) == 0 { 755 t.Log("警告: PTY 未收到输出(某些环境下 bash prompt 可能为空)") 756 } 757 t.Log("Pty.Create/Kill 验证通过") 758 } 759 760 func TestIntegrationPtySendInput(t *testing.T) { 761 c := testClient(t) 762 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 763 defer cancel() 764 765 sb := createTestSandbox(t, c, ctx) 766 t.Logf("沙箱: %s", sb.ID()) 767 768 var ( 769 mu sync.Mutex 770 output []byte 771 ) 772 773 handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24}, 774 WithOnStdout(func(data []byte) { 775 mu.Lock() 776 defer mu.Unlock() 777 output = append(output, data...) 778 }), 779 ) 780 if err != nil { 781 t.Fatalf("Pty.Create 失败: %v", err) 782 } 783 784 // 等待 PID 被设置并让 shell 初始化 785 if _, err := handle.WaitPID(ctx); err != nil { 786 t.Fatalf("WaitPID 失败: %v", err) 787 } 788 time.Sleep(2 * time.Second) 789 790 // 发送输入 791 if err := sb.Pty().SendInput(ctx, handle.PID(), []byte("echo pty-test\n")); err != nil { 792 t.Fatalf("Pty.SendInput 失败: %v", err) 793 } 794 795 // 等待输出 796 deadline := time.After(10 * time.Second) 797 for { 798 mu.Lock() 799 has := strings.Contains(string(output), "pty-test") 800 mu.Unlock() 801 if has { 802 break 803 } 804 select { 805 case <-deadline: 806 mu.Lock() 807 t.Fatalf("等待 PTY 输出超时,已收到: %q", string(output)) 808 mu.Unlock() 809 default: 810 time.Sleep(200 * time.Millisecond) 811 } 812 } 813 814 // 清理 815 _ = sb.Pty().Kill(ctx, handle.PID()) 816 _, _ = handle.Wait() 817 818 mu.Lock() 819 t.Logf("PTY 输出: %q", string(output)) 820 mu.Unlock() 821 t.Log("Pty.SendInput 验证通过") 822 } 823 824 func TestIntegrationPtyResize(t *testing.T) { 825 c := testClient(t) 826 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 827 defer cancel() 828 829 sb := createTestSandbox(t, c, ctx) 830 t.Logf("沙箱: %s", sb.ID()) 831 832 handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24}) 833 if err != nil { 834 t.Fatalf("Pty.Create 失败: %v", err) 835 } 836 837 // 等待 PID 被设置 838 if _, err := handle.WaitPID(ctx); err != nil { 839 t.Fatalf("WaitPID 失败: %v", err) 840 } 841 842 // Resize 843 if err := sb.Pty().Resize(ctx, handle.PID(), PtySize{Cols: 200, Rows: 50}); err != nil { 844 t.Fatalf("Pty.Resize 失败: %v", err) 845 } 846 t.Log("Resize 调用成功") 847 848 // 清理 849 _ = sb.Pty().Kill(ctx, handle.PID()) 850 _, _ = handle.Wait() 851 t.Log("Pty.Resize 验证通过") 852 } 853 854 // --- Filesystem.WatchDir --- 855 856 func TestIntegrationWatchDir(t *testing.T) { 857 c := testClient(t) 858 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 859 defer cancel() 860 861 sb := createTestSandbox(t, c, ctx) 862 t.Logf("沙箱: %s", sb.ID()) 863 864 // 创建监听目标目录 865 watchPath := "/tmp/watch-test-" + fmt.Sprintf("%d", time.Now().UnixNano()) 866 _, err := sb.Files().MakeDir(ctx, watchPath) 867 if err != nil { 868 t.Fatalf("MakeDir 失败: %v", err) 869 } 870 871 // 启动 WatchDir 872 watcher, err := sb.Files().WatchDir(ctx, watchPath, WithRecursive(true)) 873 if err != nil { 874 t.Fatalf("WatchDir 失败: %v", err) 875 } 876 defer watcher.Stop() 877 878 // 等待 watcher 就绪 879 time.Sleep(1 * time.Second) 880 881 // 在另一个 goroutine 写入文件触发事件 882 go func() { 883 _, _ = sb.Files().Write(ctx, watchPath+"/event-file.txt", []byte("watch me")) 884 }() 885 886 // 收集事件 887 var events []FilesystemEvent 888 deadline := time.After(15 * time.Second) 889 for { 890 select { 891 case ev, ok := <-watcher.Events(): 892 if !ok { 893 goto done 894 } 895 events = append(events, ev) 896 t.Logf("收到事件: Name=%s, Type=%s", ev.Name, ev.Type) 897 // 收到至少一个事件就可以了 898 if len(events) >= 1 { 899 goto done 900 } 901 case <-deadline: 902 goto done 903 } 904 } 905 done: 906 907 if len(events) == 0 { 908 t.Fatal("未收到任何文件系统事件") 909 } 910 t.Logf("WatchDir 验证通过: 收到 %d 个事件", len(events)) 911 } 912 913 func TestIntegrationMetadata(t *testing.T) { 914 c := testClient(t) 915 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 916 defer cancel() 917 918 templates, err := c.ListTemplates(ctx, nil) 919 if err != nil { 920 t.Fatalf("ListTemplates 失败: %v", err) 921 } 922 923 var templateID string 924 for _, tmpl := range templates { 925 if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded { 926 templateID = tmpl.TemplateID 927 break 928 } 929 } 930 if templateID == "" { 931 t.Skip("没有可用模板,跳过测试") 932 } 933 934 timeout := int32(60) 935 meta := Metadata{"env": "test", "team": "backend"} 936 sb, _, err := c.CreateAndWait(ctx, CreateParams{ 937 TemplateID: templateID, 938 Timeout: &timeout, 939 Metadata: &meta, 940 }, WithPollInterval(2*time.Second)) 941 if err != nil { 942 t.Fatalf("CreateAndWait 失败: %v", err) 943 } 944 t.Cleanup(func() { 945 killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second) 946 defer killCancel() 947 if err := sb.Kill(killCtx); err != nil { 948 t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err) 949 } 950 }) 951 952 info, err := sb.GetInfo(ctx) 953 if err != nil { 954 t.Fatalf("GetInfo 失败: %v", err) 955 } 956 957 if info.Metadata == nil { 958 t.Fatal("Metadata 应不为 nil") 959 } 960 got := *info.Metadata 961 if got["env"] != "test" { 962 t.Errorf("Metadata[env] = %q, want %q", got["env"], "test") 963 } 964 if got["team"] != "backend" { 965 t.Errorf("Metadata[team] = %q, want %q", got["team"], "backend") 966 } 967 t.Logf("Metadata 验证通过: %v", got) 968 } 969 970 // TestIntegrationCreateIdempotencyRetry 测试创建沙箱的幂等重试机制。 971 func TestIntegrationCreateIdempotencyRetry(t *testing.T) { 972 c := testClient(t) 973 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 974 defer cancel() 975 976 templates, err := c.ListTemplates(ctx, nil) 977 if err != nil { 978 t.Fatalf("ListTemplates 失败: %v", err) 979 } 980 var templateID string 981 for _, tmpl := range templates { 982 if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded { 983 templateID = tmpl.TemplateID 984 break 985 } 986 } 987 if templateID == "" { 988 t.Skip("没有可用模板,跳过测试") 989 } 990 t.Logf("使用模板: %s", templateID) 991 992 timeout := int32(60) 993 994 // 1. 不指定幂等键,验证自动生成 995 sb1, err := c.Create(ctx, CreateParams{ 996 TemplateID: templateID, 997 Timeout: &timeout, 998 }) 999 if err != nil { 1000 t.Fatalf("Create(自动幂等键)失败: %v", err) 1001 } 1002 t.Logf("沙箱1(自动幂等键): %s", sb1.ID()) 1003 defer killSandbox(t, sb1) 1004 1005 // 2. 使用相同的自定义幂等键创建两次,应返回同一沙箱 1006 idempotencyKey := "sdk-test-key-" + time.Now().Format("20060102-150405") 1007 sb2, err := c.Create(ctx, CreateParams{ 1008 TemplateID: templateID, 1009 Timeout: &timeout, 1010 IdempotencyKey: idempotencyKey, 1011 }) 1012 if err != nil { 1013 t.Fatalf("Create(幂等键首次)失败: %v", err) 1014 } 1015 t.Logf("沙箱2(幂等键 %s): %s", idempotencyKey, sb2.ID()) 1016 defer killSandbox(t, sb2) 1017 1018 sb3, err := c.Create(ctx, CreateParams{ 1019 TemplateID: templateID, 1020 Timeout: &timeout, 1021 IdempotencyKey: idempotencyKey, 1022 }) 1023 if err != nil { 1024 t.Fatalf("Create(幂等键第二次)失败: %v", err) 1025 } 1026 t.Logf("沙箱3(幂等键 %s): %s", idempotencyKey, sb3.ID()) 1027 1028 if sb2.ID() != sb3.ID() { 1029 defer killSandbox(t, sb3) 1030 t.Errorf("幂等重试应返回同一沙箱: sb2=%s, sb3=%s", sb2.ID(), sb3.ID()) 1031 } else { 1032 t.Logf("幂等重试验证通过: sb2=sb3=%s", sb2.ID()) 1033 } 1034 } 1035 1036 func killSandbox(t *testing.T, sb *Sandbox) { 1037 t.Helper() 1038 killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second) 1039 defer killCancel() 1040 if err := sb.Kill(killCtx); err != nil { 1041 t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err) 1042 } else { 1043 t.Logf("沙箱 %s 已清理", sb.ID()) 1044 } 1045 }