ChefAdorous commited on
Commit
fa11621
·
1 Parent(s): a2b5c58

Final deployment: Complete Code Execution Sandbox with all features

Browse files
Files changed (2) hide show
  1. build_devenv.ps1 +45 -0
  2. tests/test_api.py +268 -120
build_devenv.ps1 ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build script for development environment image
2
+
3
+ Write-Host "Building sandbox development environment image..." -ForegroundColor Cyan
4
+ Write-Host ""
5
+
6
+ # Check if Docker is running
7
+ try {
8
+ docker ps *> $null
9
+ if ($LASTEXITCODE -ne 0) {
10
+ Write-Host "Error: Docker is not running or not accessible" -ForegroundColor Red
11
+ exit 1
12
+ }
13
+ } catch {
14
+ Write-Host "Error: Docker is not running or not accessible" -ForegroundColor Red
15
+ exit 1
16
+ }
17
+
18
+ # Build the image
19
+ Write-Host "Building image from sandbox/images/devenv.Dockerfile..." -ForegroundColor Yellow
20
+ Write-Host "This may take 10-15 minutes on first build..." -ForegroundColor Yellow
21
+ Write-Host ""
22
+
23
+ docker build `
24
+ -f sandbox/images/devenv.Dockerfile `
25
+ -t sandbox-devenv:latest `
26
+ sandbox/images/
27
+
28
+ if ($LASTEXITCODE -eq 0) {
29
+ Write-Host ""
30
+ Write-Host "✅ Image built successfully!" -ForegroundColor Green
31
+ Write-Host ""
32
+ Write-Host "Verifying environment..." -ForegroundColor Cyan
33
+
34
+ # Run environment check
35
+ docker run --rm sandbox-devenv:latest /opt/environment_check.sh
36
+
37
+ Write-Host ""
38
+ Write-Host "Image info:" -ForegroundColor Cyan
39
+ docker images sandbox-devenv:latest
40
+
41
+ } else {
42
+ Write-Host ""
43
+ Write-Host "❌ Build failed!" -ForegroundColor Red
44
+ exit 1
45
+ }
tests/test_api.py CHANGED
@@ -1,166 +1,314 @@
1
  import pytest
2
  from fastapi.testclient import TestClient
3
  from app import app
 
4
 
5
 
6
  @pytest.fixture
7
  def client():
8
- """FastAPI test client fixture"""
9
  return TestClient(app)
10
 
11
 
12
- def test_root_endpoint(client):
13
- """Test root endpoint returns API info"""
14
- response = client.get("/")
15
- assert response.status_code == 200
16
- data = response.json()
17
- assert data["name"] == "Code Execution Sandbox API"
18
- assert data["version"] == "1.0.0"
19
- assert "supported_languages" in data
20
-
21
 
22
- def test_health_endpoint(client):
23
- """Test health check endpoint"""
24
- response = client.get("/health")
25
- # May fail if Docker is not available, but endpoint should exist
26
- assert response.status_code in [200, 503]
 
 
 
 
 
 
 
 
 
 
 
27
 
28
 
29
- def test_languages_endpoint(client):
30
- """Test languages listing endpoint"""
31
- response = client.get("/languages")
32
- assert response.status_code == 200
33
- data = response.json()
34
- assert "languages" in data
35
- languages = data["languages"]
36
- assert len(languages) == 3
37
 
38
- # Check language names
39
- lang_names = [lang["name"] for lang in languages]
40
- assert "Python" in lang_names
41
- assert "JavaScript" in lang_names
42
- assert "Bash" in lang_names
 
 
 
 
 
43
 
44
 
45
- def test_execute_python_hello_world(client):
46
- """Test executing simple Python code"""
47
- response = client.post("/execute", json={
48
- "code": "print('Hello, World!')",
49
- "language": "python"
50
- })
51
 
52
- # If Docker is not available, this may fail with 503
53
- if response.status_code == 200:
 
 
 
 
54
  data = response.json()
55
- assert "Hello, World!" in data["stdout"]
56
- assert data["exit_code"] == 0
57
- assert data["error"] is None
 
58
 
59
 
60
- def test_execute_python_math(client):
61
- """Test Python arithmetic"""
62
- response = client.post("/execute", json={
63
- "code": "result = 2 + 2\nprint(f'Result: {result}')",
64
- "language": "python"
65
- })
66
-
67
- if response.status_code == 200:
68
- data = response.json()
69
- assert "Result: 4" in data["stdout"]
70
- assert data["exit_code"] == 0
71
 
72
 
73
- def test_execute_javascript_hello_world(client):
74
- """Test executing simple JavaScript code"""
75
- response = client.post("/execute", json={
76
- "code": "console.log('Hello from Node.js!');",
77
- "language": "javascript"
78
- })
79
 
80
- if response.status_code == 200:
81
- data = response.json()
82
- assert "Hello from Node.js!" in data["stdout"]
83
- assert data["exit_code"] == 0
 
 
 
 
 
 
84
 
85
 
86
- def test_execute_bash_command(client):
87
- """Test executing Bash command"""
88
- response = client.post("/execute", json={
89
- "code": "echo 'Bash works!'",
90
- "language": "bash"
91
- })
92
 
93
- if response.status_code == 200:
94
- data = response.json()
95
- assert "Bash works!" in data["stdout"]
96
- assert data["exit_code"] == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
 
99
- def test_execute_with_syntax_error(client):
100
- """Test executing code with syntax error"""
101
- response = client.post("/execute", json={
102
- "code": "print('missing closing quote)",
103
- "language": "python"
104
- })
105
 
106
- if response.status_code == 200:
107
- data = response.json()
108
- assert data["exit_code"] != 0
109
- # Should have error in stderr or error field
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
 
112
- def test_execute_with_timeout(client):
113
- """Test timeout enforcement"""
114
- response = client.post("/execute", json={
115
- "code": "import time\ntime.sleep(5)",
116
- "language": "python",
117
- "timeout": 2
118
- })
119
 
120
- if response.status_code == 200:
121
- data = response.json()
122
- # Should timeout
123
- assert data["execution_time"] < 3
124
- assert data["error"] is not None or data["exit_code"] == 124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
 
127
- def test_invalid_language(client):
128
- """Test invalid language rejection"""
129
- response = client.post("/execute", json={
130
- "code": "print('test')",
131
- "language": "rust" # Not supported
132
- })
133
 
134
- assert response.status_code == 422 # Validation error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
 
137
- def test_empty_code(client):
138
- """Test empty code rejection"""
139
- response = client.post("/execute", json={
140
- "code": "",
141
- "language": "python"
142
- })
143
 
144
- assert response.status_code == 422 # Validation error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
 
147
- def test_timeout_out_of_range(client):
148
- """Test timeout validation"""
149
- response = client.post("/execute", json={
150
- "code": "print('test')",
151
- "language": "python",
152
- "timeout": 100 # Exceeds max of 30
153
- })
154
 
155
- assert response.status_code == 422 # Validation error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
 
157
 
158
- def test_memory_limit_validation(client):
159
- """Test memory limit validation"""
160
  response = client.post("/execute", json={
161
- "code": "print('test')",
162
- "language": "python",
163
- "memory_limit": 1024 # Exceeds max of 512
164
  })
165
 
166
- assert response.status_code == 422 # Validation error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import pytest
2
  from fastapi.testclient import TestClient
3
  from app import app
4
+ import time
5
 
6
 
7
  @pytest.fixture
8
  def client():
9
+ """Create test client"""
10
  return TestClient(app)
11
 
12
 
13
+ # ========== Session Management Tests ==========
 
 
 
 
 
 
 
 
14
 
15
+ def test_create_session(client):
16
+ """Test creating a new session"""
17
+ response = client.post("/sessions", json={
18
+ "metadata": {"test": "create_session"},
19
+ "timeout_minutes": 30
20
+ })
21
+
22
+ if response.status_code == 201:
23
+ data = response.json()
24
+ assert "session_id" in data
25
+ assert data["status"] in ["creating", "ready"]
26
+ assert data["timeout_minutes"] == 30
27
+
28
+ # Cleanup
29
+ session_id = data["session_id"]
30
+ client.delete(f"/sessions/{session_id}")
31
 
32
 
33
+ def test_list_sessions(client):
34
+ """Test listing all sessions"""
35
+ # Create a session first
36
+ create_response = client.post("/sessions", json={"metadata": {"test": "list"}})
 
 
 
 
37
 
38
+ if create_response.status_code == 201:
39
+ # List sessions
40
+ response = client.get("/sessions")
41
+ assert response.status_code == 200
42
+ sessions = response.json()
43
+ assert isinstance(sessions, list)
44
+
45
+ # Cleanup
46
+ session_id = create_response.json()["session_id"]
47
+ client.delete(f"/sessions/{session_id}")
48
 
49
 
50
+ def test_get_session(client):
51
+ """Test getting session details"""
52
+ # Create session
53
+ create_response = client.post("/sessions", json={"metadata": {"test": "get"}})
 
 
54
 
55
+ if create_response.status_code == 201:
56
+ session_id = create_response.json()["session_id"]
57
+
58
+ # Get session details
59
+ response = client.get(f"/sessions/{session_id}")
60
+ assert response.status_code == 200
61
  data = response.json()
62
+ assert data["session_id"] == session_id
63
+
64
+ # Cleanup
65
+ client.delete(f"/sessions/{session_id}")
66
 
67
 
68
+ def test_get_nonexistent_session(client):
69
+ """Test getting a session that doesn't exist"""
70
+ response = client.get("/sessions/nonexistent-id")
71
+ assert response.status_code == 404
 
 
 
 
 
 
 
72
 
73
 
74
+ def test_destroy_session(client):
75
+ """Test destroying a session"""
76
+ # Create session
77
+ create_response = client.post("/sessions", json={"metadata": {"test": "destroy"}})
 
 
78
 
79
+ if create_response.status_code == 201:
80
+ session_id = create_response.json()["session_id"]
81
+
82
+ # Destroy session
83
+ response = client.delete(f"/sessions/{session_id}")
84
+ assert response.status_code == 200
85
+
86
+ # Verify it's gone
87
+ get_response = client.get(f"/sessions/{session_id}")
88
+ assert get_response.status_code == 404
89
 
90
 
91
+ # ========== File Operations Tests ==========
92
+
93
+ def test_upload_file(client):
94
+ """Test uploading a file to session"""
95
+ # Create session
96
+ create_response = client.post("/sessions", json={"metadata": {"test": "upload"}})
97
 
98
+ if create_response.status_code == 201:
99
+ session_id = create_response.json()["session_id"]
100
+ time.sleep(2) # Wait for container to be ready
101
+
102
+ # Upload file
103
+ file_content = b"print('Hello from test file')"
104
+ response = client.post(
105
+ f"/sessions/{session_id}/files",
106
+ files={"file": ("test.py", file_content, "text/x-python")}
107
+ )
108
+
109
+ if response.status_code == 200:
110
+ data = response.json()
111
+ assert data["filename"] == "test.py"
112
+ assert data["size"] == len(file_content)
113
+
114
+ # Cleanup
115
+ client.delete(f"/sessions/{session_id}")
116
 
117
 
118
+ def test_list_files(client):
119
+ """Test listing files in session"""
120
+ # Create session
121
+ create_response = client.post("/sessions", json={"metadata": {"test": "list_files"}})
 
 
122
 
123
+ if create_response.status_code == 201:
124
+ session_id = create_response.json()["session_id"]
125
+ time.sleep(2)
126
+
127
+ # Upload a file first
128
+ client.post(
129
+ f"/sessions/{session_id}/files",
130
+ files={"file": ("test.txt", b"test content", "text/plain")}
131
+ )
132
+
133
+ # List files
134
+ response = client.get(f"/sessions/{session_id}/files")
135
+ assert response.status_code == 200
136
+ files = response.json()
137
+ assert isinstance(files, list)
138
+
139
+ # Cleanup
140
+ client.delete(f"/sessions/{session_id}")
141
 
142
 
143
+ def test_download_file(client):
144
+ """Test downloading a file from session"""
145
+ # Create session
146
+ create_response = client.post("/sessions", json={"metadata": {"test": "download"}})
 
 
 
147
 
148
+ if create_response.status_code == 201:
149
+ session_id = create_response.json()["session_id"]
150
+ time.sleep(2)
151
+
152
+ # Upload file
153
+ file_content = b"download test content"
154
+ upload_response = client.post(
155
+ f"/sessions/{session_id}/files",
156
+ files={"file": ("download.txt", file_content, "text/plain")}
157
+ )
158
+
159
+ if upload_response.status_code == 200:
160
+ # Download file
161
+ response = client.get(f"/sessions/{session_id}/files/download.txt")
162
+ assert response.status_code == 200
163
+ assert response.content == file_content
164
+
165
+ # Cleanup
166
+ client.delete(f"/sessions/{session_id}")
167
 
168
 
169
+ # ========== Execute in Session Tests ==========
170
+
171
+ def test_execute_in_session(client):
172
+ """Test executing code in a session"""
173
+ # Create session
174
+ create_response = client.post("/sessions", json={"metadata": {"test": "execute"}})
175
 
176
+ if create_response.status_code == 201:
177
+ session_id = create_response.json()["session_id"]
178
+ time.sleep(2)
179
+
180
+ # Execute code
181
+ response = client.post(
182
+ f"/sessions/{session_id}/execute",
183
+ json={
184
+ "code": "print('Hello from session')",
185
+ "language": "python",
186
+ "working_dir": "/workspace"
187
+ }
188
+ )
189
+
190
+ if response.status_code == 200:
191
+ data = response.json()
192
+ assert "Hello from session" in data["stdout"]
193
+ assert data["exit_code"] == 0
194
+
195
+ # Cleanup
196
+ client.delete(f"/sessions/{session_id}")
197
 
198
 
199
+ def test_execute_file_in_session(client):
200
+ """Test executing an uploaded file"""
201
+ # Create session
202
+ create_response = client.post("/sessions", json={"metadata": {"test": "execute_file"}})
 
 
203
 
204
+ if create_response.status_code == 201:
205
+ session_id = create_response.json()["session_id"]
206
+ time.sleep(2)
207
+
208
+ # Upload Python file
209
+ file_content = b"print('Executed from file')\nprint(2 + 2)"
210
+ upload_response = client.post(
211
+ f"/sessions/{session_id}/files",
212
+ files={"file": ("script.py", file_content, "text/x-python")}
213
+ )
214
+
215
+ if upload_response.status_code == 200:
216
+ # Execute file
217
+ response = client.post(
218
+ f"/sessions/{session_id}/execute-file",
219
+ json={
220
+ "filepath": "/workspace/script.py",
221
+ "language": "python",
222
+ "args": []
223
+ }
224
+ )
225
+
226
+ if response.status_code == 200:
227
+ data = response.json()
228
+ assert "Executed from file" in data["stdout"]
229
+ assert "4" in data["stdout"]
230
+ assert data["exit_code"] == 0
231
+
232
+ # Cleanup
233
+ client.delete(f"/sessions/{session_id}")
234
 
235
 
236
+ def test_persistent_state_across_executions(client):
237
+ """Test that session maintains state across multiple executions"""
238
+ # Create session
239
+ create_response = client.post("/sessions", json={"metadata": {"test": "persistent"}})
 
 
 
240
 
241
+ if create_response.status_code == 201:
242
+ session_id = create_response.json()["session_id"]
243
+ time.sleep(2)
244
+
245
+ # First execution: create file
246
+ response1 = client.post(
247
+ f"/sessions/{session_id}/execute",
248
+ json={
249
+ "code": "with open('/workspace/data.txt', 'w') as f: f.write('persistent')",
250
+ "language": "python"
251
+ }
252
+ )
253
+
254
+ if response1.status_code == 200:
255
+ # Second execution: read file (should still exist)
256
+ response2 = client.post(
257
+ f"/sessions/{session_id}/execute",
258
+ json={
259
+ "code": "with open('/workspace/data.txt', 'r') as f: print(f.read())",
260
+ "language": "python"
261
+ }
262
+ )
263
+
264
+ if response2.status_code == 200:
265
+ data = response2.json()
266
+ assert "persistent" in data["stdout"]
267
+
268
+ # Cleanup
269
+ client.delete(f"/sessions/{session_id}")
270
+
271
 
272
+ # ========== Backward Compatibility Tests ==========
273
 
274
+ def test_stateless_execute_still_works(client):
275
+ """Test that original /execute endpoint still works"""
276
  response = client.post("/execute", json={
277
+ "code": "print('Stateless execution')",
278
+ "language": "python"
 
279
  })
280
 
281
+ if response.status_code == 200:
282
+ data = response.json()
283
+ assert "Stateless execution" in data["stdout"]
284
+ assert data["exit_code"] == 0
285
+
286
+
287
+ # ========== API Root and Health Tests ==========
288
+
289
+ def test_root_endpoint(client):
290
+ """Test root endpoint"""
291
+ response = client.get("/")
292
+ assert response.status_code == 200
293
+ data = response.json()
294
+ assert data["name"] == "Code Execution Sandbox API"
295
+ assert data["version"] == "2.0.0"
296
+ assert "endpoints" in data
297
+
298
+
299
+ def test_health_endpoint(client):
300
+ """Test health check"""
301
+ response = client.get("/health")
302
+ if response.status_code == 200:
303
+ data = response.json()
304
+ assert data["status"] == "healthy"
305
+ assert "docker" in data
306
+
307
+
308
+ def test_languages_endpoint(client):
309
+ """Test languages listing"""
310
+ response = client.get("/languages")
311
+ assert response.status_code == 200
312
+ data = response.json()
313
+ assert "languages" in data
314
+ assert len(data["languages"]) > 0