Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/python/cocalc-api/tests/test_jupyter.py
5578 views
1
"""
2
Tests for Jupyter kernel functionality.
3
4
Note: These tests are skipped in CI environments due to unreliable Jupyter setup
5
in containerized environments. They are thoroughly tested locally.
6
"""
7
8
import os
9
import pytest
10
11
# Import helper from conftest
12
from tests.conftest import retry_with_backoff
13
14
# Skip all Jupyter tests in CI (GitHub Actions)
15
# Jupyter kernel startup and execution is unreliable in containerized CI environments
16
pytestmark = pytest.mark.skipif(os.environ.get("CI") == "true", reason="Jupyter tests skipped in CI due to environment constraints")
17
18
19
class TestJupyterKernelSetup:
20
"""Tests for Jupyter kernel installation and availability."""
21
22
def test_install_ipykernel(self, project_client):
23
"""Test installing ipykernel in the project."""
24
# Install ipykernel package
25
result = project_client.system.exec(
26
command="python3",
27
args=["-m", "pip", "install", "ipykernel"],
28
timeout=120, # 2 minutes should be enough for pip install
29
)
30
31
# Check that installation succeeded
32
assert result["exit_code"] == 0
33
assert "stderr" in result
34
35
def test_install_jupyter_kernel(self, project_client):
36
"""Test installing the Python 3 Jupyter kernel."""
37
# Install the kernel spec
38
result = project_client.system.exec(
39
command="python3",
40
args=[
41
"-m",
42
"ipykernel",
43
"install",
44
"--user", # Install to user location, not system
45
"--name=python3",
46
"--display-name=Python 3",
47
],
48
timeout=30,
49
)
50
51
# Check that kernel installation succeeded
52
assert result["exit_code"] == 0
53
54
55
class TestJupyterKernels:
56
"""Tests for Jupyter kernel availability."""
57
58
def test_kernels_list_with_project(self, hub, temporary_project):
59
"""Test getting kernel specs for a specific project."""
60
project_id = temporary_project["project_id"]
61
kernels = hub.jupyter.kernels(project_id=project_id)
62
63
# Should return a list of kernel specs
64
assert isinstance(kernels, list)
65
assert len(kernels) > 0
66
67
def test_python3_kernel_available(self, hub, temporary_project):
68
"""Test that the python3 kernel is available after installation."""
69
project_id = temporary_project["project_id"]
70
kernels = hub.jupyter.kernels(project_id=project_id)
71
72
# Extract kernel names from the list
73
kernel_names = [k.get("name") for k in kernels if isinstance(k, dict)]
74
assert "python3" in kernel_names
75
76
77
class TestJupyterExecuteViaHub:
78
"""Tests for executing code via hub.jupyter.execute()."""
79
80
def test_execute_simple_sum(self, hub, temporary_project):
81
"""Test executing a simple sum using the python3 kernel.
82
83
Note: First execution may take longer as kernel needs to start up (30+ seconds).
84
In CI environments, this can take even longer, so we use more retries.
85
"""
86
project_id = temporary_project["project_id"]
87
88
result = retry_with_backoff(lambda: hub.jupyter.execute(input="sum(range(100))", kernel="python3", project_id=project_id),
89
max_retries=5,
90
retry_delay=10)
91
92
# Check the result structure
93
assert isinstance(result, dict)
94
assert "output" in result
95
96
# Check that we got the correct result (sum of 0..99 = 4950)
97
output = result["output"]
98
assert len(output) > 0
99
100
# Extract the result from the output
101
# Format: [{'data': {'text/plain': '4950'}}]
102
first_output = output[0]
103
assert "data" in first_output
104
assert "text/plain" in first_output["data"]
105
assert first_output["data"]["text/plain"] == "4950"
106
107
def test_execute_with_history(self, hub, temporary_project):
108
"""Test executing code with history context."""
109
project_id = temporary_project["project_id"]
110
111
result = retry_with_backoff(
112
lambda: hub.jupyter.execute(history=["a = 100"], input="sum(range(a + 1))", kernel="python3", project_id=project_id))
113
114
# Check the result (sum of 0..100 = 5050)
115
assert isinstance(result, dict)
116
assert "output" in result
117
118
output = result["output"]
119
assert len(output) > 0
120
121
first_output = output[0]
122
assert "data" in first_output
123
assert "text/plain" in first_output["data"]
124
assert first_output["data"]["text/plain"] == "5050"
125
126
def test_execute_print_statement(self, hub, temporary_project):
127
"""Test executing code that prints output.
128
129
Note: First execution may take longer as kernel needs to start up (30+ seconds).
130
"""
131
project_id = temporary_project["project_id"]
132
133
result = retry_with_backoff(lambda: hub.jupyter.execute(input='print("Hello from Jupyter")', kernel="python3", project_id=project_id))
134
135
# Check that we got output
136
assert isinstance(result, dict)
137
assert "output" in result
138
139
output = result["output"]
140
assert len(output) > 0
141
142
# Print statements produce stream output
143
first_output = output[0]
144
assert "name" in first_output
145
assert first_output["name"] == "stdout"
146
assert "text" in first_output
147
assert "Hello from Jupyter" in first_output["text"]
148
149
150
class TestJupyterExecuteViaProject:
151
"""Tests for executing code via project.system.jupyter_execute()."""
152
153
def test_jupyter_execute_simple_sum(self, project_client):
154
"""
155
Test executing a simple sum via project API.
156
157
The result is a list of output items directly (not wrapped in a dict).
158
159
Note: First execution may take longer as kernel needs to start up (30+ seconds).
160
"""
161
result = retry_with_backoff(lambda: project_client.system.jupyter_execute(input="sum(range(100))", kernel="python3"))
162
163
# Result is a list, not a dict with 'output' key
164
assert isinstance(result, list)
165
assert len(result) > 0
166
167
# Check that we got the correct result (sum of 0..99 = 4950)
168
first_output = result[0]
169
assert "data" in first_output
170
assert "text/plain" in first_output["data"]
171
assert first_output["data"]["text/plain"] == "4950"
172
173
def test_jupyter_execute_with_history(self, project_client):
174
"""
175
Test executing code with history via project API.
176
177
The result is a list of output items directly.
178
179
Note: First execution may take longer as kernel needs to start up (30+ seconds).
180
"""
181
result = retry_with_backoff(lambda: project_client.system.jupyter_execute(history=["b = 50"], input="b * 2", kernel="python3"))
182
183
# Result is a list
184
assert isinstance(result, list)
185
assert len(result) > 0
186
187
# Check the result (50 * 2 = 100)
188
first_output = result[0]
189
assert "data" in first_output
190
assert "text/plain" in first_output["data"]
191
assert first_output["data"]["text/plain"] == "100"
192
193
def test_jupyter_execute_list_operation(self, project_client):
194
"""
195
Test executing code that works with lists.
196
197
The result is a list of output items directly.
198
"""
199
result = retry_with_backoff(lambda: project_client.system.jupyter_execute(input="[x**2 for x in range(5)]", kernel="python3"))
200
201
# Result is a list
202
assert isinstance(result, list)
203
assert len(result) > 0
204
205
# Check the result ([0, 1, 4, 9, 16])
206
first_output = result[0]
207
assert "data" in first_output
208
assert "text/plain" in first_output["data"]
209
assert first_output["data"]["text/plain"] == "[0, 1, 4, 9, 16]"
210
211
212
class TestJupyterKernelManagement:
213
"""Tests for Jupyter kernel management (list and stop kernels)."""
214
215
def test_list_jupyter_kernels(self, project_client):
216
"""Test listing running Jupyter kernels."""
217
# First execute some code to ensure a kernel is running
218
retry_with_backoff(lambda: project_client.system.jupyter_execute(input="1+1", kernel="python3"))
219
220
# List kernels
221
kernels = project_client.system.list_jupyter_kernels()
222
223
# Should return a list
224
assert isinstance(kernels, list)
225
226
# Should have at least one kernel running (from previous tests)
227
assert len(kernels) > 0
228
229
# Each kernel should have required fields
230
for kernel in kernels:
231
assert "pid" in kernel
232
assert "connectionFile" in kernel
233
assert isinstance(kernel["pid"], int)
234
assert isinstance(kernel["connectionFile"], str)
235
236
def test_stop_jupyter_kernel(self, project_client):
237
"""Test stopping a specific Jupyter kernel."""
238
# Execute code to start a kernel
239
retry_with_backoff(lambda: project_client.system.jupyter_execute(input="1+1", kernel="python3"))
240
241
# List kernels
242
kernels = project_client.system.list_jupyter_kernels()
243
assert len(kernels) > 0
244
245
# Stop the first kernel
246
kernel_to_stop = kernels[0]
247
result = project_client.system.stop_jupyter_kernel(pid=kernel_to_stop["pid"])
248
249
# Should return success
250
assert isinstance(result, dict)
251
assert "success" in result
252
assert result["success"] is True
253
254
# Verify kernel is no longer in the list
255
kernels_after = project_client.system.list_jupyter_kernels()
256
remaining_pids = [k["pid"] for k in kernels_after]
257
assert kernel_to_stop["pid"] not in remaining_pids
258
259