Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/python/cocalc-api/tests/test_project.py
5609 views
1
"""
2
Tests for Project client functionality.
3
"""
4
import os
5
import pytest
6
7
from cocalc_api import Project
8
from .conftest import assert_valid_uuid
9
10
11
class TestProjectCreation:
12
"""Tests for project creation and management."""
13
14
def test_create_temporary_project(self, temporary_project):
15
"""Test that a temporary project is created successfully."""
16
assert temporary_project is not None
17
assert 'project_id' in temporary_project
18
assert 'title' in temporary_project
19
assert 'description' in temporary_project
20
assert temporary_project['title'].startswith('CoCalc API Test ')
21
assert temporary_project['description'] == "Temporary project created by cocalc-api tests"
22
# Project ID should be a valid UUID
23
assert_valid_uuid(temporary_project['project_id'], "Project ID")
24
25
def test_project_exists_in_list(self, hub, temporary_project):
26
"""Test that the created project appears in the projects list."""
27
projects = hub.projects.get(all=True)
28
project_ids = [p['project_id'] for p in projects]
29
assert temporary_project['project_id'] in project_ids
30
31
32
class TestProjectSystem:
33
"""Tests for Project system operations."""
34
35
def test_ping(self, project_client):
36
"""Test basic ping connectivity to project."""
37
result = project_client.system.ping()
38
assert result is not None
39
assert isinstance(result, dict)
40
41
def test_project_initialization(self, api_key, cocalc_host):
42
"""Test Project client initialization."""
43
project_id = "test-project-id"
44
project = Project(project_id=project_id, api_key=api_key, host=cocalc_host)
45
assert project.project_id == project_id
46
assert project.api_key == api_key
47
assert project.host == cocalc_host
48
assert project.client is not None
49
50
def test_project_with_temporary_project(self, project_client, temporary_project):
51
"""Test Project client using the temporary project."""
52
assert project_client.project_id == temporary_project['project_id']
53
# Test that we can ping the specific project
54
result = project_client.system.ping()
55
assert result is not None
56
assert isinstance(result, dict)
57
58
def test_exec_command(self, project_client):
59
"""Test executing shell commands in the project."""
60
# Test running 'date -Is' to get ISO date with seconds
61
result = project_client.system.exec(command="date", args=["-Is"])
62
63
# Check the result structure
64
assert 'stdout' in result
65
assert 'stderr' in result
66
assert 'exit_code' in result
67
68
# Should succeed
69
assert result['exit_code'] == 0
70
71
# Should have minimal stderr
72
assert result['stderr'] == '' or len(result['stderr']) == 0
73
74
# Parse the returned date and compare with current time
75
from datetime import datetime
76
import re
77
78
date_output = result['stdout'].strip()
79
# Expected format: 2025-09-29T12:34:56+00:00 or similar
80
81
# Check if the output matches ISO format
82
iso_pattern = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$'
83
assert re.match(iso_pattern, date_output), f"Date output '{date_output}' doesn't match ISO format"
84
85
# Parse the date from the command output
86
# Remove the timezone for comparison (date -Is includes timezone)
87
date_part = date_output[:19] # Take YYYY-MM-DDTHH:MM:SS part
88
remote_time = datetime.fromisoformat(date_part)
89
90
# Get current time
91
current_time = datetime.now()
92
93
# Check if the times are close (within 60 seconds)
94
time_diff = abs((current_time - remote_time).total_seconds())
95
assert time_diff < 60, f"Time difference too large: {time_diff} seconds. Remote: {date_output}, Local: {current_time.isoformat()}"
96
97
def test_exec_stderr_and_exit_code(self, project_client):
98
"""Test executing a command that writes to stderr and returns a specific exit code."""
99
# Use bash to echo to stderr and exit with code 42
100
bash_script = "echo 'test error message' >&2; exit 42"
101
102
# The API raises an exception for non-zero exit codes
103
# but includes the stderr and exit code information in the error message
104
with pytest.raises(RuntimeError) as exc_info:
105
project_client.system.exec(command=bash_script, bash=True)
106
107
error_message = str(exc_info.value)
108
109
# Verify the error message contains expected information
110
assert "exited with nonzero code 42" in error_message
111
assert "stderr='test error message" in error_message
112
113
# Extract and verify the stderr content is properly captured
114
import re
115
stderr_match = re.search(r"stderr='([^']*)'", error_message)
116
assert stderr_match is not None, "Could not find stderr in error message"
117
stderr_content = stderr_match.group(1).strip()
118
assert stderr_content == "test error message"
119
120
def test_list_jupyter_kernels(self, project_client):
121
"""Test listing Jupyter kernels in a project."""
122
result = project_client.system.list_jupyter_kernels()
123
assert isinstance(result, list)
124
print(f"✓ Found {len(result)} Jupyter kernels")
125
# Each kernel should have basic properties
126
for kernel in result:
127
assert "pid" in kernel
128
assert isinstance(kernel["pid"], int)
129
assert kernel["pid"] > 0
130
131
@pytest.mark.skipif(os.environ.get("CI") == "true", reason="Jupyter tests skipped in CI due to environment constraints")
132
def test_stop_jupyter_kernel(self, project_client):
133
"""Test stopping a Jupyter kernel.
134
135
Note: Skipped in CI environments due to unreliable Jupyter setup.
136
"""
137
from tests.conftest import retry_with_backoff
138
139
# First, execute code to ensure a kernel is running with retry
140
retry_with_backoff(lambda: project_client.system.jupyter_execute(input="1+1", kernel="python3"))
141
142
# List kernels
143
kernels = project_client.system.list_jupyter_kernels()
144
assert len(kernels) > 0, "Expected at least one kernel to be running"
145
146
# Stop the first kernel
147
pid = kernels[0]["pid"]
148
result = project_client.system.stop_jupyter_kernel(pid=pid)
149
assert isinstance(result, dict)
150
assert "success" in result
151
print(f"✓ Stopped Jupyter kernel with PID {pid}: {result}")
152
153