Skip to content

Commit f1be5f0

Browse files
Python runtime sandbox and python sdk e2e test
1 parent 2332dd1 commit f1be5f0

File tree

4 files changed

+739
-9
lines changed

4 files changed

+739
-9
lines changed

dev/tools/test-e2e

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,57 @@ import sys
1919

2020
from shared import utils
2121

22+
def run_go_e2e_tests(repo_root, artifact_dir, env):
23+
""" Runs the Go e2e tests """
24+
print("========= Running Go E2E tests... =========")
25+
go_test_cmd = utils.go_tool_args(
26+
"gotestsum",
27+
f"--junitfile={artifact_dir}/e2e-go-junit.xml",
28+
f"--jsonfile={artifact_dir}/test-e2e.json",
29+
"--", "./test/e2e/...", "--parallel=1", "-v"
30+
)
31+
print(f"Running command: {' '.join(go_test_cmd)}")
32+
return subprocess.run(go_test_cmd, env=env, cwd=repo_root).returncode
33+
34+
def setup_python_sdk(repo_root, env):
35+
""" Installs the Python SDK and its dependencies """
36+
print("========= Ensuring Python SDK is installed... =========")
37+
sdk_path = os.path.join(repo_root, "clients", "python", "agentic-sandbox-client")
38+
# Install in editable mode
39+
install_cmd = [sys.executable, "-m", "pip", "install", "-e", ".[test]"]
40+
print(f"Running command: {' '.join(install_cmd)} in {sdk_path}")
41+
result = subprocess.run(install_cmd, env=env, cwd=sdk_path, check=False)
42+
if result.returncode != 0:
43+
print("Failed to install Python SDK.")
44+
return False
45+
print("Python SDK installed successfully.")
46+
return True
47+
48+
def run_python_e2e_tests(repo_root, artifact_dir, env):
49+
""" Runs the Python e2e tests and generates a junit report """
50+
print("========= Running Python SDK E2E tests... =========")
51+
52+
# Define the path for the JUnit report
53+
junit_report_path = f"{artifact_dir}/e2e-python-sdk-junit.xml"
54+
55+
# Set the environment variable for the test namespace
56+
test_env = env.copy()
57+
# Ensure the python client can be imported
58+
python_path = os.path.join(repo_root, "clients", "python", "agentic-sandbox-client")
59+
test_env["PYTHONPATH"] = f"{python_path}:{test_env.get('PYTHONPATH', '')}"
60+
61+
pytest_cmd = [
62+
sys.executable,
63+
"-m",
64+
"pytest",
65+
f"--junitxml={junit_report_path}",
66+
"-v",
67+
os.path.join(repo_root, "test", "e2e/")
68+
]
69+
70+
print(f"Running command: {' '.join(pytest_cmd)}")
71+
result = subprocess.run(pytest_cmd, env=test_env, cwd=repo_root)
72+
return result.returncode
2273

2374
def main():
2475
""" invokes unit tests and outputs a junit results file """
@@ -27,17 +78,30 @@ def main():
2778
artifact_dir = os.getenv("ARTIFACTS")
2879
if not artifact_dir:
2980
artifact_dir = f"{repo_root}/bin"
81+
os.makedirs(artifact_dir, exist_ok=True)
3082

3183
env = os.environ.copy()
3284
env["IMAGE_TAG"] = utils.get_image_tag()
3385

34-
result = subprocess.run(utils.go_tool_args(
35-
"gotestsum",
36-
f"--junitfile={repo_root}/bin/e2e-junit.xml",
37-
f"--jsonfile={artifact_dir}/test-e2e.json",
38-
"--", "./test/e2e/...", "--parallel=1"
39-
), env=env, cwd=repo_root)
40-
return result.returncode
86+
if not setup_python_sdk(repo_root, env):
87+
return 1
88+
89+
go_test_result = run_go_e2e_tests(repo_root, artifact_dir, env)
90+
python_test_result = run_python_e2e_tests(repo_root, artifact_dir, env)
91+
92+
if go_test_result != 0:
93+
print("Go E2E tests failed.")
94+
95+
if python_test_result != 0:
96+
print("Python SDK E2E tests failed.")
97+
return 1
98+
99+
if go_test_result != 0 or python_test_result != 0:
100+
print("One or more E2E tests failed.")
101+
return 1
102+
103+
print("All E2E tests passed.")
104+
return 0
41105

42106

43107
if __name__ == "__main__":
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright 2025 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
import os
17+
import yaml
18+
import asyncio
19+
import time
20+
from kubernetes import client, config
21+
from kubernetes.client import ApiException
22+
from agentic_sandbox import SandboxClient
23+
24+
# Get namespace from environment variable, default to 'default'
25+
NAMESPACE = os.getenv("TEST_NAMESPACE", "default")
26+
IMAGE_TAG = os.getenv("IMAGE_TAG", "latest")
27+
28+
TEMPLATE_NAME = "e2e-pytest-template"
29+
30+
TEMPLATE_MANIFEST = f"""
31+
apiVersion: extensions.agents.x-k8s.io/v1alpha1
32+
kind: SandboxTemplate
33+
metadata:
34+
name: {TEMPLATE_NAME}
35+
namespace: {NAMESPACE}
36+
spec:
37+
podTemplate:
38+
metadata:
39+
labels:
40+
app: python-sandbox-pytest
41+
spec:
42+
containers:
43+
- name: python-sandbox
44+
image: kind.local/python-runtime-sandbox:{IMAGE_TAG}
45+
imagePullPolicy: IfNotPresent
46+
ports:
47+
- containerPort: 8888
48+
"""
49+
50+
WARMPOOL_NAME = "e2e-pytest-warmpool"
51+
52+
WARMPOOL_MANIFEST = f"""
53+
apiVersion: extensions.agents.x-k8s.io/v1alpha1
54+
kind: SandboxWarmPool
55+
metadata:
56+
name: {WARMPOOL_NAME}
57+
namespace: {NAMESPACE}
58+
spec:
59+
replicas: 1
60+
sandboxTemplateRef:
61+
name: {TEMPLATE_NAME}
62+
"""
63+
64+
@pytest.fixture(scope="module")
65+
def k8s_api():
66+
try:
67+
config.load_incluster_config()
68+
except config.ConfigException:
69+
config.load_kube_config()
70+
api = client.CustomObjectsApi()
71+
return api
72+
73+
@pytest.fixture(scope="module", autouse=True)
74+
def create_template(k8s_api):
75+
template_body = yaml.safe_load(TEMPLATE_MANIFEST)
76+
try:
77+
k8s_api.create_namespaced_custom_object(
78+
group="extensions.agents.x-k8s.io",
79+
version="v1alpha1",
80+
namespace=NAMESPACE,
81+
plural="sandboxtemplates",
82+
body=template_body,
83+
)
84+
print(f"SandboxTemplate '{TEMPLATE_NAME}' created in namespace '{NAMESPACE}'.")
85+
except client.ApiException as e:
86+
if e.status == 409:
87+
print(f"SandboxTemplate '{TEMPLATE_NAME}' already exists.")
88+
else:
89+
raise
90+
91+
yield
92+
93+
try:
94+
k8s_api.delete_namespaced_custom_object(
95+
group="extensions.agents.x-k8s.io",
96+
version="v1alpha1",
97+
namespace=NAMESPACE,
98+
plural="sandboxtemplates",
99+
name=TEMPLATE_NAME,
100+
)
101+
print(f"SandboxTemplate '{TEMPLATE_NAME}' deleted.")
102+
except client.ApiException as e:
103+
if e.status == 404:
104+
print(f"SandboxTemplate '{TEMPLATE_NAME}' not found for deletion.")
105+
else:
106+
print(f"Error deleting SandboxTemplate '{TEMPLATE_NAME}': {e}")
107+
108+
@pytest.fixture(scope="function")
109+
def create_warmpool(k8s_api, create_template): # Depends on create_template
110+
warmpool_body = yaml.safe_load(WARMPOOL_MANIFEST)
111+
try:
112+
k8s_api.create_namespaced_custom_object(
113+
group="extensions.agents.x-k8s.io",
114+
version="v1alpha1",
115+
namespace=NAMESPACE,
116+
plural="sandboxwarmpools",
117+
body=warmpool_body,
118+
)
119+
print(f"SandboxWarmPool '{WARMPOOL_NAME}' created.")
120+
except ApiException as e:
121+
if e.status == 409:
122+
print(f"SandboxWarmPool '{WARMPOOL_NAME}' already exists.")
123+
else:
124+
raise
125+
126+
# Wait for warmpool to be ready
127+
for i in range(30): # Wait up to 60 seconds
128+
try:
129+
warmpool = k8s_api.get_namespaced_custom_object(
130+
group="extensions.agents.x-k8s.io",
131+
version="v1alpha1",
132+
namespace=NAMESPACE,
133+
plural="sandboxwarmpools",
134+
name=WARMPOOL_NAME,
135+
)
136+
if warmpool.get("status", {}).get("readyReplicas", 0) >= 1:
137+
print(f"SandboxWarmPool '{WARMPOOL_NAME}' is ready with {warmpool['status']['readyReplicas']} replicas.")
138+
break
139+
except ApiException as e:
140+
print(f"Error getting warmpool status: {e}")
141+
if i == 29:
142+
pytest.fail(f"SandboxWarmPool '{WARMPOOL_NAME}' did not become ready in time.")
143+
time.sleep(2)
144+
145+
yield
146+
147+
try:
148+
k8s_api.delete_namespaced_custom_object(
149+
group="extensions.agents.x-k8s.io",
150+
version="v1alpha1",
151+
namespace=NAMESPACE,
152+
plural="sandboxwarmpools",
153+
name=WARMPOOL_NAME,
154+
)
155+
print(f"SandboxWarmPool '{WARMPOOL_NAME}' deleted.")
156+
except ApiException as e:
157+
if e.status == 404:
158+
print(f"SandboxWarmPool '{WARMPOOL_NAME}' not found for deletion.")
159+
else:
160+
print(f"Error deleting SandboxWarmPool '{WARMPOOL_NAME}': {e}")
161+
162+
@pytest.mark.asyncio
163+
async def test_python_sdk_sandbox_execution():
164+
"""
165+
Tests the Python SDK SandboxClient.
166+
"""
167+
print(f"--- Running test_python_sdk_sandbox_execution in namespace {NAMESPACE} ---")
168+
try:
169+
with SandboxClient(template_name=TEMPLATE_NAME, namespace=NAMESPACE) as sandbox:
170+
await asyncio.sleep(60) # Wait for the sandbox to be fully ready
171+
172+
print("\n--- Testing Command Execution ---")
173+
command_to_run = "echo 'Hello from pytest sandbox!'"
174+
print(f"Executing command: '{command_to_run}'")
175+
176+
result = sandbox.run(command_to_run)
177+
178+
print(f"Stdout: {result.stdout.strip()}")
179+
print(f"Stderr: {result.stderr.strip()}")
180+
print(f"Exit Code: {result.exit_code}")
181+
182+
assert result.exit_code == 0
183+
assert result.stdout.strip() == "Hello from pytest sandbox!"
184+
print("--- Command Execution Test Passed! ---")
185+
186+
print("\n--- Testing File Write/Read ---")
187+
file_path = "test_sdk.txt"
188+
file_content = "SDK test content."
189+
sandbox.write(file_path, file_content)
190+
read_content = sandbox.read(file_path).decode('utf-8')
191+
assert read_content == file_content
192+
print("--- File Write/Read Test Passed! ---")
193+
194+
except Exception as e:
195+
print(f"\n--- An error occurred during the test: {e} ---")
196+
pytest.fail(f"Test failed due to exception: {e}")
197+
finally:
198+
print("--- Test finished ---")
199+
200+
# @pytest.mark.asyncio
201+
# async def test_python_sdk_warmpool_execution(create_warmpool): # Use the warmpool fixture
202+
# """
203+
# Tests the Python SDK SandboxClient with a WarmPool.
204+
# """
205+
# print(f"--- Running test_python_sdk_warmpool_execution in namespace {NAMESPACE} ---")
206+
# try:
207+
# with SandboxClient(template_name=TEMPLATE_NAME, namespace=NAMESPACE) as sandbox:
208+
# print("\n--- Testing Command Execution (WarmPool) ---")
209+
# command_to_run = "echo 'Hello from warmpooled sandbox!'"
210+
# print(f"Executing command: '{command_to_run}'")
211+
212+
# result = sandbox.run(command_to_run)
213+
214+
# print(f"Stdout: {result.stdout.strip()}")
215+
# assert result.exit_code == 0
216+
# assert result.stdout.strip() == "Hello from warmpooled sandbox!"
217+
# print("--- Command Execution Test Passed (WarmPool)! ---")
218+
219+
# except Exception as e:
220+
# print(f"\n--- An error occurred during the test: {e} ---")
221+
# pytest.fail(f"Test failed due to exception: {e}")
222+
# finally:
223+
# print("--- WarmPool Test finished ---")
224+
225+
# pip install pytest pytest-asyncio kubernetes requests PyYAML

0 commit comments

Comments
 (0)