scalebox-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. scalebox/__init__.py +80 -0
  2. scalebox/api/__init__.py +128 -0
  3. scalebox/api/client/__init__.py +8 -0
  4. scalebox/api/client/api/__init__.py +1 -0
  5. scalebox/api/client/api/sandboxes/__init__.py +0 -0
  6. scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
  12. scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
  13. scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
  14. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  15. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
  16. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
  17. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
  18. scalebox/api/client/client.py +288 -0
  19. scalebox/api/client/errors.py +16 -0
  20. scalebox/api/client/models/__init__.py +81 -0
  21. scalebox/api/client/models/build_log_entry.py +79 -0
  22. scalebox/api/client/models/created_access_token.py +100 -0
  23. scalebox/api/client/models/created_team_api_key.py +166 -0
  24. scalebox/api/client/models/error.py +67 -0
  25. scalebox/api/client/models/identifier_masking_details.py +83 -0
  26. scalebox/api/client/models/listed_sandbox.py +138 -0
  27. scalebox/api/client/models/log_level.py +11 -0
  28. scalebox/api/client/models/new_access_token.py +59 -0
  29. scalebox/api/client/models/new_sandbox.py +125 -0
  30. scalebox/api/client/models/new_team_api_key.py +59 -0
  31. scalebox/api/client/models/node.py +154 -0
  32. scalebox/api/client/models/node_detail.py +152 -0
  33. scalebox/api/client/models/node_status.py +11 -0
  34. scalebox/api/client/models/node_status_change.py +61 -0
  35. scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  36. scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  37. scalebox/api/client/models/resumed_sandbox.py +68 -0
  38. scalebox/api/client/models/sandbox.py +125 -0
  39. scalebox/api/client/models/sandbox_detail.py +178 -0
  40. scalebox/api/client/models/sandbox_log.py +70 -0
  41. scalebox/api/client/models/sandbox_logs.py +73 -0
  42. scalebox/api/client/models/sandbox_metric.py +110 -0
  43. scalebox/api/client/models/sandbox_state.py +9 -0
  44. scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
  45. scalebox/api/client/models/team.py +83 -0
  46. scalebox/api/client/models/team_api_key.py +158 -0
  47. scalebox/api/client/models/team_user.py +68 -0
  48. scalebox/api/client/models/template.py +179 -0
  49. scalebox/api/client/models/template_build.py +117 -0
  50. scalebox/api/client/models/template_build_file_upload.py +70 -0
  51. scalebox/api/client/models/template_build_request.py +115 -0
  52. scalebox/api/client/models/template_build_request_v2.py +88 -0
  53. scalebox/api/client/models/template_build_start_v2.py +114 -0
  54. scalebox/api/client/models/template_build_status.py +11 -0
  55. scalebox/api/client/models/template_step.py +91 -0
  56. scalebox/api/client/models/template_update_request.py +59 -0
  57. scalebox/api/client/models/update_team_api_key.py +59 -0
  58. scalebox/api/client/py.typed +1 -0
  59. scalebox/api/client/types.py +46 -0
  60. scalebox/api/metadata.py +19 -0
  61. scalebox/cli.py +125 -0
  62. scalebox/client/__init__.py +0 -0
  63. scalebox/client/aclient.py +57 -0
  64. scalebox/client/api.proto +460 -0
  65. scalebox/client/buf.gen.yaml +8 -0
  66. scalebox/client/client.py +102 -0
  67. scalebox/client/requirements.txt +5 -0
  68. scalebox/code_interpreter/__init__.py +12 -0
  69. scalebox/code_interpreter/charts.py +230 -0
  70. scalebox/code_interpreter/code_interpreter_async.py +369 -0
  71. scalebox/code_interpreter/code_interpreter_sync.py +317 -0
  72. scalebox/code_interpreter/constants.py +3 -0
  73. scalebox/code_interpreter/exceptions.py +13 -0
  74. scalebox/code_interpreter/models.py +485 -0
  75. scalebox/connection_config.py +92 -0
  76. scalebox/csx_connect/__init__.py +1 -0
  77. scalebox/csx_connect/client.py +485 -0
  78. scalebox/csx_desktop/__init__.py +0 -0
  79. scalebox/csx_desktop/main.py +651 -0
  80. scalebox/exceptions.py +83 -0
  81. scalebox/generated/__init__.py +0 -0
  82. scalebox/generated/api.py +61 -0
  83. scalebox/generated/api_pb2.py +203 -0
  84. scalebox/generated/api_pb2.pyi +956 -0
  85. scalebox/generated/api_pb2_connect.py +1456 -0
  86. scalebox/generated/rpc.py +50 -0
  87. scalebox/generated/versions.py +3 -0
  88. scalebox/requirements.txt +36 -0
  89. scalebox/sandbox/__init__.py +0 -0
  90. scalebox/sandbox/commands/__init__.py +0 -0
  91. scalebox/sandbox/commands/command_handle.py +69 -0
  92. scalebox/sandbox/commands/main.py +39 -0
  93. scalebox/sandbox/filesystem/__init__.py +0 -0
  94. scalebox/sandbox/filesystem/filesystem.py +95 -0
  95. scalebox/sandbox/filesystem/watch_handle.py +60 -0
  96. scalebox/sandbox/main.py +139 -0
  97. scalebox/sandbox/sandbox_api.py +91 -0
  98. scalebox/sandbox/signature.py +40 -0
  99. scalebox/sandbox/utils.py +34 -0
  100. scalebox/sandbox_async/__init__.py +1 -0
  101. scalebox/sandbox_async/commands/command.py +307 -0
  102. scalebox/sandbox_async/commands/command_handle.py +187 -0
  103. scalebox/sandbox_async/commands/pty.py +187 -0
  104. scalebox/sandbox_async/filesystem/filesystem.py +557 -0
  105. scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
  106. scalebox/sandbox_async/main.py +646 -0
  107. scalebox/sandbox_async/sandbox_api.py +365 -0
  108. scalebox/sandbox_async/utils.py +7 -0
  109. scalebox/sandbox_sync/__init__.py +2 -0
  110. scalebox/sandbox_sync/commands/__init__.py +0 -0
  111. scalebox/sandbox_sync/commands/command.py +300 -0
  112. scalebox/sandbox_sync/commands/command_handle.py +150 -0
  113. scalebox/sandbox_sync/commands/pty.py +181 -0
  114. scalebox/sandbox_sync/filesystem/__init__.py +0 -0
  115. scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
  116. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
  117. scalebox/sandbox_sync/main.py +790 -0
  118. scalebox/sandbox_sync/sandbox_api.py +356 -0
  119. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
  120. scalebox/test/README.md +329 -0
  121. scalebox/test/__init__.py +0 -0
  122. scalebox/test/aclient.py +72 -0
  123. scalebox/test/code_interpreter_centext.py +21 -0
  124. scalebox/test/code_interpreter_centext_sync.py +21 -0
  125. scalebox/test/code_interpreter_test.py +34 -0
  126. scalebox/test/code_interpreter_test_sync.py +34 -0
  127. scalebox/test/run_all_validation_tests.py +334 -0
  128. scalebox/test/run_code_interpreter_tests.sh +67 -0
  129. scalebox/test/run_tests.sh +230 -0
  130. scalebox/test/test_basic.py +78 -0
  131. scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
  132. scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
  133. scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
  134. scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
  135. scalebox/test/test_e2b_first.py +11 -0
  136. scalebox/test/test_sandbox_async_comprehensive.py +738 -0
  137. scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
  138. scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
  139. scalebox/test/test_sandbox_usage_examples.py +987 -0
  140. scalebox/test/testacreate.py +24 -0
  141. scalebox/test/testagetinfo.py +18 -0
  142. scalebox/test/testcodeinterpreter_async.py +508 -0
  143. scalebox/test/testcodeinterpreter_sync.py +239 -0
  144. scalebox/test/testcomputeuse.py +243 -0
  145. scalebox/test/testnovnc.py +12 -0
  146. scalebox/test/testsandbox_async.py +118 -0
  147. scalebox/test/testsandbox_sync.py +38 -0
  148. scalebox/utils/__init__.py +0 -0
  149. scalebox/utils/httpcoreclient.py +297 -0
  150. scalebox/utils/httpxclient.py +403 -0
  151. scalebox/version.py +16 -0
  152. scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
  153. scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
  154. scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
  155. scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  156. scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  157. scalebox_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,738 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Comprehensive validation test for sandbox_async module.
4
+
5
+ This test suite demonstrates and validates all key functionality of the AsyncSandbox:
6
+ - Sandbox lifecycle management (create, connect, kill)
7
+ - File system operations (read, write, list, remove, etc.)
8
+ - Command execution (foreground, background, PTY)
9
+ - Static methods and class methods
10
+ - Error handling and edge cases
11
+ - Performance testing
12
+ """
13
+
14
+ import asyncio
15
+ import datetime
16
+ import logging
17
+ import os
18
+ import tempfile
19
+ import time
20
+ from doctest import debug
21
+ from io import BytesIO, StringIO
22
+ from typing import List, Optional
23
+
24
+ from scalebox.exceptions import SandboxException
25
+ from scalebox.sandbox.commands.command_handle import CommandExitException, PtySize
26
+ from scalebox.sandbox.filesystem.filesystem import EntryInfo, FileType, WriteInfo
27
+ from scalebox.sandbox_async.main import AsyncSandbox
28
+
29
+ # 配置日志
30
+ logging.basicConfig(
31
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
32
+ )
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class AsyncSandboxValidator:
37
+ """Comprehensive AsyncSandbox validation test suite."""
38
+
39
+ def __init__(self):
40
+ self.sandbox: Optional[AsyncSandbox] = None
41
+ self.test_results = []
42
+ self.failed_tests = []
43
+
44
+ async def log_test_result(
45
+ self, test_name: str, success: bool, message: str = "", duration: float = 0
46
+ ):
47
+ """记录测试结果"""
48
+ status = "✅ PASS" if success else "❌ FAIL"
49
+ result = {
50
+ "test": test_name,
51
+ "success": success,
52
+ "message": message,
53
+ "duration": duration,
54
+ }
55
+ self.test_results.append(result)
56
+
57
+ if not success:
58
+ self.failed_tests.append(test_name)
59
+
60
+ logger.info(f"{status} {test_name} ({duration:.3f}s) {message}")
61
+
62
+ async def run_test(self, test_func, test_name: str):
63
+ """运行单个测试并记录结果"""
64
+ start_time = time.time()
65
+ try:
66
+ await test_func()
67
+ duration = time.time() - start_time
68
+ await self.log_test_result(test_name, True, duration=duration)
69
+ except Exception as e:
70
+ duration = time.time() - start_time
71
+ await self.log_test_result(test_name, False, str(e), duration=duration)
72
+
73
+ # ======================== 基础沙箱操作测试 ========================
74
+
75
+ async def test_sandbox_creation(self):
76
+ """测试沙箱创建"""
77
+ self.sandbox = await AsyncSandbox.create(
78
+ template="base",
79
+ debug=True,
80
+ timeout=300,
81
+ api_key="sk-Wk4IgtUYOqnttxGaxZmELEV4p2FXh15Evt0FIcSa",
82
+ metadata={"test": "async_validation"},
83
+ envs={"TEST_ENV": "async_test"},
84
+ )
85
+ assert self.sandbox is not None
86
+ assert self.sandbox.sandbox_id is not None
87
+ logger.info(f"Created sandbox with ID: {self.sandbox.sandbox_id}")
88
+
89
+ async def test_sandbox_is_running(self):
90
+ """测试沙箱运行状态检查"""
91
+ assert self.sandbox is not None
92
+ is_running = await self.sandbox.is_running()
93
+ assert is_running == True
94
+ logger.info(f"Sandbox is running: {is_running}")
95
+
96
+ async def test_sandbox_get_info(self):
97
+ """测试获取沙箱信息"""
98
+ assert self.sandbox is not None
99
+ info = await self.sandbox.get_info()
100
+ assert info is not None
101
+ assert info.sandbox_id == self.sandbox.sandbox_id
102
+ logger.info(f"Sandbox info: {info}")
103
+
104
+ async def test_sandbox_set_timeout(self):
105
+ """测试设置沙箱超时"""
106
+ assert self.sandbox is not None
107
+ await self.sandbox.set_timeout(600) # 设置10分钟超时
108
+ logger.info("Successfully set sandbox timeout to 600 seconds")
109
+
110
+ async def test_sandbox_get_metrics(self):
111
+ """测试获取沙箱指标"""
112
+ assert self.sandbox is not None
113
+ try:
114
+ metrics = await self.sandbox.get_metrics()
115
+ print(metrics)
116
+ logger.info(f"Sandbox metrics: {len(metrics) if metrics else 0} entries")
117
+ except Exception as e:
118
+ # 某些版本可能不支持指标
119
+ logger.warning(f"Metrics not supported: {e}")
120
+
121
+ # ======================== 文件系统操作测试 ========================
122
+
123
+ async def test_filesystem_write_text(self):
124
+ """测试写入文本文件"""
125
+ assert self.sandbox is not None
126
+ test_content = "Hello, AsyncSandbox!\nThis is a test file.\n测试中文内容"
127
+
128
+ result = await self.sandbox.files.write("/tmp/test_async.txt", test_content)
129
+ print(result)
130
+ assert isinstance(result, WriteInfo)
131
+ assert result.path == "/tmp/test_async.txt"
132
+ assert result.type == FileType.FILE
133
+ logger.info(f"Written file: {result}")
134
+
135
+ async def test_filesystem_write_bytes(self):
136
+ """测试写入字节文件"""
137
+ assert self.sandbox is not None
138
+ test_content = b"Binary content: \x00\x01\x02\x03\xff"
139
+
140
+ result = await self.sandbox.files.write("/tmp/test_binary.bin", test_content)
141
+ print(result)
142
+ assert isinstance(result, WriteInfo)
143
+ assert result.path == "/tmp/test_binary.bin"
144
+
145
+ async def test_filesystem_write_io(self):
146
+ """测试写入IO对象"""
147
+ assert self.sandbox is not None
148
+
149
+ # 测试StringIO
150
+ string_io = StringIO("Content from StringIO\nSecond line")
151
+ result1 = await self.sandbox.files.write("/tmp/test_stringio.txt", string_io)
152
+ print(result1)
153
+ assert isinstance(result1, WriteInfo)
154
+
155
+ # 测试BytesIO
156
+ bytes_io = BytesIO(b"Content from BytesIO")
157
+ result2 = await self.sandbox.files.write("/tmp/test_bytesio.bin", bytes_io)
158
+ print(result2)
159
+ assert isinstance(result2, WriteInfo)
160
+
161
+ async def test_filesystem_write_multiple(self):
162
+ """测试批量写入文件"""
163
+ assert self.sandbox is not None
164
+
165
+ files = [
166
+ {"path": "/tmp/multi1.txt", "data": "Content of file 1"},
167
+ {"path": "/tmp/multi2.txt", "data": "Content of file 2"},
168
+ {"path": "/tmp/multi3.bin", "data": b"Binary content of file 3"},
169
+ ]
170
+
171
+ results = await self.sandbox.files.write(files)
172
+ print(results)
173
+ assert isinstance(results, list)
174
+ assert len(results) == 3
175
+ for result in results:
176
+ assert isinstance(result, WriteInfo)
177
+
178
+ async def test_filesystem_read_text(self):
179
+ """测试读取文本文件"""
180
+ assert self.sandbox is not None
181
+
182
+ content = await self.sandbox.files.read("/tmp/test_async.txt", format="text")
183
+ print(content)
184
+ assert isinstance(content, str)
185
+ assert "Hello, AsyncSandbox!" in content
186
+ assert "测试中文内容" in content
187
+
188
+ async def test_filesystem_read_bytes(self):
189
+ """测试读取字节文件"""
190
+ assert self.sandbox is not None
191
+
192
+ content = await self.sandbox.files.read("/tmp/test_binary.bin", format="bytes")
193
+ print(content)
194
+ assert isinstance(content, bytearray)
195
+ expected = b"Binary content: \x00\x01\x02\x03\xff"
196
+ assert bytes(content) == expected
197
+
198
+ async def test_filesystem_read_stream(self):
199
+ """测试流式读取文件"""
200
+ assert self.sandbox is not None
201
+
202
+ stream = await self.sandbox.files.read("/tmp/test_async.txt", format="stream")
203
+ chunks = []
204
+ async for chunk in stream:
205
+ chunks.append(chunk)
206
+
207
+ content = b"".join(chunks).decode("utf-8")
208
+ print(content)
209
+ assert "Hello, AsyncSandbox!" in content
210
+
211
+ async def test_filesystem_list(self):
212
+ """测试列出目录内容"""
213
+ assert self.sandbox is not None
214
+
215
+ entries = await self.sandbox.files.list("/workspace/tmp", depth=1)
216
+ print(entries)
217
+ assert isinstance(entries, list)
218
+ assert len(entries) > 0
219
+
220
+ # 检查我们创建的文件是否存在
221
+ filenames = [entry.name for entry in entries]
222
+ assert "test_async.txt" in filenames
223
+
224
+ for entry in entries:
225
+ assert isinstance(entry, EntryInfo)
226
+ assert entry.name is not None
227
+ assert entry.path is not None
228
+
229
+ async def test_filesystem_exists(self):
230
+ """测试文件存在性检查"""
231
+ assert self.sandbox is not None
232
+
233
+ # 检查存在的文件
234
+ exists = await self.sandbox.files.exists("/workspace/tmp/test_async.txt")
235
+ print(exists)
236
+ assert exists == True
237
+
238
+ # 检查不存在的文件
239
+ not_exists = await self.sandbox.files.exists("/workspace/tmp/nonexistent.txt")
240
+ print(not_exists)
241
+ assert not_exists == False
242
+
243
+ async def test_filesystem_get_info(self):
244
+ """测试获取文件信息"""
245
+ assert self.sandbox is not None
246
+
247
+ info = await self.sandbox.files.get_info("/workspace/tmp/test_async.txt")
248
+ print(info)
249
+ assert isinstance(info, EntryInfo)
250
+ assert info.name == "test_async.txt"
251
+ assert info.type == FileType.FILE
252
+ assert info.path == "/workspace/tmp/test_async.txt"
253
+ assert info.size > 0
254
+
255
+ async def test_filesystem_make_dir(self):
256
+ """测试创建目录"""
257
+ assert self.sandbox is not None
258
+
259
+ # 创建新目录
260
+ result = await self.sandbox.files.make_dir("/workspace/tmp/test_dir/nested_dir")
261
+ print(result)
262
+ assert result == True
263
+
264
+ # 再次创建同一目录应该返回False
265
+ result = await self.sandbox.files.make_dir("/workspace/tmp/test_dir")
266
+ print(result)
267
+ assert result == False
268
+
269
+ async def test_filesystem_rename(self):
270
+ """测试重命名文件"""
271
+ assert self.sandbox is not None
272
+
273
+ # 创建一个测试文件
274
+ await self.sandbox.files.write("/tmp/old_name.txt", "content to rename")
275
+
276
+ # 重命名
277
+ result = await self.sandbox.files.rename(
278
+ "/workspace/tmp/old_name.txt", "/workspace/tmp/new_name.txt"
279
+ )
280
+ print(result)
281
+ assert isinstance(result, EntryInfo)
282
+ assert result.name == "new_name.txt"
283
+
284
+ # 确认旧文件不存在,新文件存在
285
+ assert await self.sandbox.files.exists("/workspace/tmp/old_name.txt") == False
286
+ assert await self.sandbox.files.exists("/workspace/tmp/new_name.txt") == True
287
+
288
+ async def test_filesystem_remove(self):
289
+ """测试删除文件和目录"""
290
+ assert self.sandbox is not None
291
+
292
+ # 删除文件
293
+ await self.sandbox.files.remove("/workspace/tmp/new_name.txt")
294
+ assert await self.sandbox.files.exists("/workspace/tmp/new_name.txt") == False
295
+
296
+ # 删除目录
297
+ await self.sandbox.files.remove("/workspace/tmp/test_dir")
298
+ assert await self.sandbox.files.exists("/workspace/tmp/test_dir") == False
299
+
300
+ # ======================== 命令执行测试 ========================
301
+
302
+ async def test_command_run_foreground(self):
303
+ """测试前台命令执行"""
304
+ assert self.sandbox is not None
305
+
306
+ result = await self.sandbox.commands.run("echo 'Hello from command'")
307
+ print(result)
308
+ assert result.exit_code == 0
309
+ assert "Hello from command" in result.stdout
310
+ assert result.stderr == ""
311
+
312
+ async def test_command_run_with_error(self):
313
+ """测试执行失败的命令"""
314
+ assert self.sandbox is not None
315
+ try:
316
+ await self.sandbox.commands.run("ls /nonexistent_directory 2>&1")
317
+ except CommandExitException as e:
318
+ print(e)
319
+ assert e.exit_code != 0
320
+ assert (
321
+ "No such file or directory" in e.stdout or "cannot access" in e.stdout
322
+ )
323
+
324
+ async def test_command_run_background(self):
325
+ """测试后台命令执行"""
326
+ assert self.sandbox is not None
327
+
328
+ # 启动后台命令
329
+ handle = await self.sandbox.commands.run(
330
+ "sleep 2 && echo 'Background task completed'", background=True
331
+ )
332
+
333
+ # 等待命令完成
334
+ result = await handle.wait()
335
+ print(result)
336
+ assert result.exit_code == 0
337
+ assert "Background task completed" in result.stdout
338
+
339
+ async def test_command_with_env_and_cwd(self):
340
+ """测试带环境变量和工作目录的命令"""
341
+ assert self.sandbox is not None
342
+
343
+ # 创建测试目录
344
+ await self.sandbox.files.make_dir("/tmp/test_cwd")
345
+
346
+ result = await self.sandbox.commands.run(
347
+ "echo $TEST_VAR && pwd",
348
+ envs={"TEST_VAR": "test_value"},
349
+ cwd="/tmp/test_cwd",
350
+ )
351
+ print(result)
352
+ assert result.exit_code == 0
353
+ assert "test_value" in result.stdout
354
+ assert "/tmp/test_cwd" in result.stdout
355
+
356
+ async def test_command_with_callbacks(self):
357
+ """测试带回调的命令执行"""
358
+ assert self.sandbox is not None
359
+
360
+ stdout_data = []
361
+ stderr_data = []
362
+
363
+ async def stdout_handler(data):
364
+ stdout_data.append(data)
365
+
366
+ async def stderr_handler(data):
367
+ stderr_data.append(data)
368
+
369
+ result = await self.sandbox.commands.run(
370
+ "echo 'stdout message' && echo 'stderr message' >&2",
371
+ on_stdout=stdout_handler,
372
+ on_stderr=stderr_handler,
373
+ )
374
+
375
+ assert result.exit_code == 0
376
+ # Note: 回调可能在命令结束后才触发,这里只验证命令成功执行
377
+
378
+ async def test_command_list(self):
379
+ """测试列出运行中的命令"""
380
+ assert self.sandbox is not None
381
+
382
+ # 启动一个长时间运行的后台命令
383
+ handle = await self.sandbox.commands.run("sleep 30", background=True)
384
+ print(handle)
385
+ # 列出进程
386
+ processes = await self.sandbox.commands.list()
387
+ print(processes)
388
+ assert isinstance(processes, list)
389
+
390
+ # 查找我们的进程
391
+ found = any(p.pid == handle.pid for p in processes)
392
+ assert found == True
393
+
394
+ # 清理:杀死进程
395
+ killed = await self.sandbox.commands.kill(handle.pid)
396
+ print(killed)
397
+ assert killed == True
398
+
399
+ async def test_command_send_stdin(self):
400
+ """测试向命令发送标准输入"""
401
+ assert self.sandbox is not None
402
+
403
+ # 启动一个等待输入的命令
404
+ handle = await self.sandbox.commands.run("cat", background=True)
405
+ print(handle)
406
+
407
+ # 发送输入
408
+ await self.sandbox.commands.send_stdin(handle.pid, "Hello stdin\n")
409
+ print("111111111111")
410
+ # 等待一点时间让命令处理输入
411
+ await asyncio.sleep(1)
412
+
413
+ # 杀死进程并获取结果
414
+ await self.sandbox.commands.kill(handle.pid)
415
+ print("22222222")
416
+ try:
417
+ result = await handle.wait()
418
+ except CommandExitException as e:
419
+ print(e)
420
+
421
+ # cat命令会被SIGKILL杀死,所以exit_code不会是0
422
+ # 但我们验证了send_stdin没有抛出异常
423
+
424
+ async def test_command_connect(self):
425
+ """测试连接到运行中的命令"""
426
+ assert self.sandbox is not None
427
+
428
+ # 启动后台命令
429
+ handle1 = await self.sandbox.commands.run("sleep 5", background=True)
430
+ print(handle1)
431
+ # 连接到同一个进程
432
+ handle2 = await self.sandbox.commands.connect(handle1.pid)
433
+ print(handle2)
434
+ assert handle1.pid == handle2.pid
435
+
436
+ # 清理
437
+ await self.sandbox.commands.kill(handle1.pid)
438
+
439
+ # ======================== PTY操作测试 ========================
440
+
441
+ async def test_pty_create(self):
442
+ """测试创建PTY"""
443
+ assert self.sandbox is not None
444
+
445
+ pty_data = []
446
+
447
+ async def pty_handler(data):
448
+ pty_data.append(data)
449
+
450
+ pty_handle = await self.sandbox.pty.create(
451
+ size=PtySize(rows=24, cols=80),
452
+ on_data=pty_handler,
453
+ cwd="/tmp",
454
+ envs={"PTY_TEST": "value"},
455
+ )
456
+
457
+ assert pty_handle is not None
458
+ assert pty_handle.pid > 0
459
+
460
+ # 发送命令并等待
461
+ await self.sandbox.pty.send_stdin(pty_handle.pid, b"echo 'PTY test'\n")
462
+ await asyncio.sleep(2)
463
+
464
+ # 调整大小
465
+ await self.sandbox.pty.resize(pty_handle.pid, PtySize(rows=30, cols=100))
466
+
467
+ # 清理
468
+ killed = await self.sandbox.pty.kill(pty_handle.pid)
469
+ assert killed == True
470
+
471
+ # ======================== 静态方法测试 ========================
472
+
473
+ async def test_static_methods(self):
474
+ """测试静态方法"""
475
+ assert self.sandbox is not None
476
+ sandbox_id = self.sandbox.sandbox_id
477
+
478
+ # 静态方法获取信息
479
+ info = await AsyncSandbox.get_info(sandbox_id)
480
+ assert info.sandbox_id == sandbox_id
481
+
482
+ # 静态方法设置超时
483
+ await AsyncSandbox.set_timeout(sandbox_id, 900)
484
+
485
+ # 静态方法获取指标(可能不支持)
486
+ try:
487
+ metrics = await AsyncSandbox.get_metrics(sandbox_id)
488
+ logger.info(f"Static method metrics: {len(metrics) if metrics else 0}")
489
+ except Exception as e:
490
+ logger.warning(f"Static method metrics not supported: {e}")
491
+
492
+ # ======================== 错误处理测试 ========================
493
+
494
+ async def test_error_handling_file_operations(self):
495
+ """测试文件操作错误处理"""
496
+ assert self.sandbox is not None
497
+
498
+ # 读取不存在的文件
499
+ try:
500
+ await self.sandbox.files.read("/nonexistent/file.txt")
501
+ assert False, "应该抛出异常"
502
+ except Exception as e:
503
+ print(type(e).__name__)
504
+ logger.info(f"正确捕获文件读取错误: {type(e).__name__}")
505
+
506
+ # 写入到只读目录
507
+ try:
508
+ await self.sandbox.files.write("/proc/test.txt", "content")
509
+ assert False, "应该抛出异常"
510
+ except Exception as e:
511
+ print(type(e).__name__)
512
+ logger.info(f"正确捕获文件写入错误: {type(e).__name__}")
513
+
514
+ async def test_error_handling_command_operations(self):
515
+ """测试命令操作错误处理"""
516
+ assert self.sandbox is not None
517
+
518
+ # 杀死不存在的进程
519
+ killed = await self.sandbox.commands.kill(99999)
520
+ print(killed)
521
+ assert killed == False
522
+
523
+ # 连接到不存在的进程
524
+ try:
525
+ await self.sandbox.commands.connect(99999)
526
+ assert False, "应该抛出异常"
527
+ except Exception as e:
528
+ print(type(e).__name__)
529
+ logger.info(f"正确捕获进程连接错误: {type(e).__name__}")
530
+
531
+ # ======================== 性能测试 ========================
532
+
533
+ async def test_performance_file_operations(self):
534
+ """测试文件操作性能"""
535
+ assert self.sandbox is not None
536
+
537
+ # 批量文件操作
538
+ start_time = time.time()
539
+
540
+ # 创建100个小文件
541
+ files = [
542
+ {"path": f"/tmp/perf_test_{i}.txt", "data": f"Content of file {i}"}
543
+ for i in range(100)
544
+ ]
545
+
546
+ await self.sandbox.files.write(files)
547
+ duration = time.time() - start_time
548
+
549
+ logger.info(f"Created 100 files in {duration:.3f}s")
550
+ assert duration < 30 # 应该在30秒内完成
551
+
552
+ # 清理
553
+ for i in range(100):
554
+ try:
555
+ await self.sandbox.files.remove(f"/workspace/tmp/perf_test_{i}.txt")
556
+ except:
557
+ pass
558
+
559
+ async def test_performance_command_operations(self):
560
+ """测试命令操作性能"""
561
+ assert self.sandbox is not None
562
+
563
+ start_time = time.time()
564
+
565
+ # 执行10个并发命令
566
+ tasks = []
567
+ for i in range(10):
568
+ task = self.sandbox.commands.run(f"echo 'Command {i}'")
569
+ tasks.append(task)
570
+
571
+ results = await asyncio.gather(*tasks)
572
+ duration = time.time() - start_time
573
+
574
+ print(f"Executed 10 concurrent commands in {duration:.3f}s")
575
+
576
+ # 验证所有命令都成功
577
+ for i, result in enumerate(results):
578
+ assert result.exit_code == 0
579
+ assert f"Command {i}" in result.stdout
580
+
581
+ # ======================== 连接测试 ========================
582
+
583
+ async def test_sandbox_connect(self):
584
+ """测试连接到现有沙箱"""
585
+ assert self.sandbox is not None
586
+ original_id = self.sandbox.sandbox_id
587
+
588
+ # 连接到现有沙箱
589
+ connected_sandbox = await AsyncSandbox.connect(original_id)
590
+ print(connected_sandbox)
591
+ assert connected_sandbox.sandbox_id == original_id
592
+
593
+ # 验证连接的沙箱可以正常使用
594
+ is_running = await connected_sandbox.is_running()
595
+ logger.info(f"Connected sandbox running status: {is_running}")
596
+
597
+ # 在连接的沙箱中执行操作
598
+ result = await connected_sandbox.commands.run("echo 'Connected sandbox test'")
599
+ print(result)
600
+ assert result.exit_code == 0
601
+ assert "Connected sandbox test" in result.stdout
602
+
603
+ # ======================== 上下文管理器测试 ========================
604
+
605
+ async def test_context_manager(self):
606
+ """测试上下文管理器"""
607
+ # 使用上下文管理器创建临时沙箱
608
+ async with await AsyncSandbox.create(debug=True) as temp_sandbox:
609
+ assert temp_sandbox is not None
610
+
611
+ # 在上下文中使用沙箱
612
+ result = await temp_sandbox.commands.run("echo 'Context manager test'")
613
+ print(result)
614
+ assert result.exit_code == 0
615
+
616
+ temp_id = temp_sandbox.sandbox_id
617
+
618
+ # 沙箱应该已经被自动清理
619
+ # (注意:这里可能需要等待一段时间才能验证)
620
+
621
+ # ======================== 主测试执行器 ========================
622
+
623
+ async def run_all_tests(self):
624
+ """运行所有测试"""
625
+ logger.info("开始AsyncSandbox综合验证测试...")
626
+
627
+ # 基础操作测试
628
+ await self.run_test(self.test_sandbox_creation, "Sandbox Creation")
629
+ await self.run_test(self.test_sandbox_is_running, "Sandbox Running Status")
630
+ await self.run_test(self.test_sandbox_get_info, "Sandbox Get Info")
631
+ await self.run_test(self.test_sandbox_set_timeout, "Sandbox Set Timeout")
632
+ await self.run_test(self.test_sandbox_get_metrics, "Sandbox Get Metrics")
633
+
634
+ # 文件系统操作测试
635
+ await self.run_test(self.test_filesystem_write_text, "Filesystem Write Text")
636
+ await self.run_test(self.test_filesystem_write_bytes, "Filesystem Write Bytes")
637
+ await self.run_test(self.test_filesystem_write_io, "Filesystem Write IO")
638
+ await self.run_test(
639
+ self.test_filesystem_write_multiple, "Filesystem Write Multiple"
640
+ )
641
+ await self.run_test(self.test_filesystem_read_text, "Filesystem Read Text")
642
+ await self.run_test(self.test_filesystem_read_bytes, "Filesystem Read Bytes")
643
+ await self.run_test(self.test_filesystem_read_stream, "Filesystem Read Stream")
644
+ await self.run_test(self.test_filesystem_list, "Filesystem List")
645
+ await self.run_test(self.test_filesystem_exists, "Filesystem Exists")
646
+ await self.run_test(self.test_filesystem_get_info, "Filesystem Get Info")
647
+ await self.run_test(self.test_filesystem_make_dir, "Filesystem Make Dir")
648
+ await self.run_test(self.test_filesystem_rename, "Filesystem Rename")
649
+ await self.run_test(self.test_filesystem_remove, "Filesystem Remove")
650
+
651
+ # 命令执行测试
652
+ await self.run_test(self.test_command_run_foreground, "Command Run Foreground")
653
+ await self.run_test(self.test_command_run_with_error, "Command Run With Error")
654
+ await self.run_test(self.test_command_run_background, "Command Run Background")
655
+ await self.run_test(
656
+ self.test_command_with_env_and_cwd, "Command With Env And CWD"
657
+ )
658
+ await self.run_test(self.test_command_with_callbacks, "Command With Callbacks")
659
+ await self.run_test(self.test_command_list, "Command List")
660
+ await self.run_test(self.test_command_send_stdin, "Command Send Stdin")
661
+ await self.run_test(self.test_command_connect, "Command Connect")
662
+
663
+ # PTY测试
664
+ await self.run_test(self.test_pty_create, "PTY Create")
665
+
666
+ # 静态方法测试
667
+ await self.run_test(self.test_static_methods, "Static Methods")
668
+
669
+ # 错误处理测试
670
+ await self.run_test(
671
+ self.test_error_handling_file_operations, "Error Handling File Operations"
672
+ )
673
+ await self.run_test(
674
+ self.test_error_handling_command_operations,
675
+ "Error Handling Command Operations",
676
+ )
677
+
678
+ # 性能测试
679
+ await self.run_test(
680
+ self.test_performance_file_operations, "Performance File Operations"
681
+ )
682
+ await self.run_test(
683
+ self.test_performance_command_operations, "Performance Command Operations"
684
+ )
685
+
686
+ # 连接测试
687
+ await self.run_test(self.test_sandbox_connect, "Sandbox Connect")
688
+
689
+ # 上下文管理器测试
690
+ await self.run_test(self.test_context_manager, "Context Manager")
691
+
692
+ async def cleanup(self):
693
+ """清理资源"""
694
+ if self.sandbox:
695
+ try:
696
+ await self.sandbox.kill()
697
+ logger.info("Sandbox cleaned up successfully")
698
+ except Exception as e:
699
+ logger.error(f"Error cleaning up sandbox: {e}")
700
+
701
+ def print_summary(self):
702
+ """打印测试摘要"""
703
+ total_tests = len(self.test_results)
704
+ passed_tests = sum(1 for r in self.test_results if r["success"])
705
+ failed_tests = total_tests - passed_tests
706
+
707
+ total_duration = sum(r["duration"] for r in self.test_results)
708
+
709
+ print("\n" + "=" * 60)
710
+ print("AsyncSandbox综合验证测试报告")
711
+ print("=" * 60)
712
+ print(f"总测试数: {total_tests}")
713
+ print(f"通过数: {passed_tests}")
714
+ print(f"失败数: {failed_tests}")
715
+ print(f"总耗时: {total_duration:.3f}秒")
716
+ print(f"成功率: {(passed_tests/total_tests*100):.1f}%")
717
+
718
+ if self.failed_tests:
719
+ print(f"\n失败的测试:")
720
+ for test in self.failed_tests:
721
+ print(f" ❌ {test}")
722
+
723
+ print("=" * 60)
724
+
725
+
726
+ async def main():
727
+ """主函数"""
728
+ validator = AsyncSandboxValidator()
729
+
730
+ try:
731
+ await validator.run_all_tests()
732
+ finally:
733
+ await validator.cleanup()
734
+ validator.print_summary()
735
+
736
+
737
+ if __name__ == "__main__":
738
+ asyncio.run(main())