Add per-client Makefiles for C, Python, and Go with: - CLI mode: Tests --help, arg parsing, syntax validation - Library mode: Unit tests, import verification - Integration mode: API contract validation (with credentials) - Functional mode: Real-world scenario tests C client: - 22 library tests (SHA-256, HMAC-SHA256, detect_language) - Full unsandbox.c implementation with examples Python client (sync + async): - Delegates to sync/ and async/ subdirectories - pytest-based test suites with coverage - Examples for concurrent execution, streaming Go client: - Delegates to sync/ and async/ subdirectories - go test integration with vet and fmt Also update detect-changes.sh to detect changes in both root-level un.* files AND clients/ directory.
210 lines
6.9 KiB
Python
210 lines
6.9 KiB
Python
"""
|
|
Tests for async operations (requires mocking or real API access)
|
|
"""
|
|
|
|
import pytest
|
|
import asyncio
|
|
import sys
|
|
import os
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
|
|
import un_async
|
|
from un_async import (
|
|
execute_code,
|
|
execute_async,
|
|
get_job,
|
|
wait_for_job,
|
|
cancel_job,
|
|
list_jobs,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_credentials(monkeypatch):
|
|
"""Provide mock credentials."""
|
|
monkeypatch.setenv("UNSANDBOX_PUBLIC_KEY", "test_public_key")
|
|
monkeypatch.setenv("UNSANDBOX_SECRET_KEY", "test_secret_key")
|
|
|
|
|
|
class TestAsyncOperations:
|
|
"""Test async API operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_code_completed(self, mock_credentials):
|
|
"""Test execute_code when job completes immediately."""
|
|
mock_response = {
|
|
"job_id": "job_123",
|
|
"status": "completed",
|
|
"stdout": "Hello World\n",
|
|
"stderr": "",
|
|
"exit_code": 0,
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
# First call for execute, then wait_for_job polls
|
|
mock_req.side_effect = [
|
|
mock_response, # execute_code calls _make_request
|
|
]
|
|
|
|
result = await execute_code("python", "print('Hello World')")
|
|
|
|
assert result["status"] == "completed"
|
|
assert "Hello World" in result["stdout"]
|
|
assert result["exit_code"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_async_returns_job_id(self, mock_credentials):
|
|
"""Test execute_async returns job_id immediately."""
|
|
mock_response = {
|
|
"job_id": "job_456",
|
|
"status": "pending",
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.return_value = mock_response
|
|
|
|
job_id = await execute_async("python", "print('test')")
|
|
|
|
assert job_id == "job_456"
|
|
mock_req.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_job(self, mock_credentials):
|
|
"""Test getting job status."""
|
|
mock_response = {
|
|
"job_id": "job_789",
|
|
"status": "running",
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.return_value = mock_response
|
|
|
|
result = await get_job("job_789")
|
|
|
|
assert result["job_id"] == "job_789"
|
|
assert result["status"] == "running"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wait_for_job_polling(self, mock_credentials):
|
|
"""Test wait_for_job polls until completion."""
|
|
# Simulate polling: pending -> running -> completed
|
|
mock_responses = [
|
|
{"job_id": "job_100", "status": "pending"},
|
|
{"job_id": "job_100", "status": "running"},
|
|
{"job_id": "job_100", "status": "completed", "stdout": "done"},
|
|
]
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.side_effect = mock_responses
|
|
|
|
with patch("asyncio.sleep", new_callable=AsyncMock):
|
|
result = await wait_for_job("job_100")
|
|
|
|
assert result["status"] == "completed"
|
|
assert result["stdout"] == "done"
|
|
assert mock_req.call_count == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_job(self, mock_credentials):
|
|
"""Test cancelling a job."""
|
|
mock_response = {
|
|
"job_id": "job_kill",
|
|
"status": "cancelled",
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.return_value = mock_response
|
|
|
|
result = await cancel_job("job_kill")
|
|
|
|
assert result["status"] == "cancelled"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_jobs(self, mock_credentials):
|
|
"""Test listing all jobs."""
|
|
mock_response = {
|
|
"jobs": [
|
|
{"id": "job_1", "status": "completed"},
|
|
{"id": "job_2", "status": "running"},
|
|
]
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.return_value = mock_response
|
|
|
|
jobs = await list_jobs()
|
|
|
|
assert len(jobs) == 2
|
|
assert jobs[0]["id"] == "job_1"
|
|
assert jobs[1]["id"] == "job_2"
|
|
|
|
|
|
class TestAsyncConcurrency:
|
|
"""Test concurrent async operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_executions(self, mock_credentials):
|
|
"""Test running multiple executions concurrently."""
|
|
mock_response = {
|
|
"job_id": "job_concurrent",
|
|
"status": "completed",
|
|
"stdout": "result",
|
|
}
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.return_value = mock_response
|
|
|
|
# Run 3 concurrent executions
|
|
results = await asyncio.gather(
|
|
execute_code("python", "print(1)"),
|
|
execute_code("javascript", "console.log(2)"),
|
|
execute_code("bash", "echo 3"),
|
|
)
|
|
|
|
assert len(results) == 3
|
|
assert all(r["status"] == "completed" for r in results)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_with_different_statuses(self, mock_credentials):
|
|
"""Test concurrent operations with different final statuses."""
|
|
responses = [
|
|
{"job_id": "1", "status": "completed", "stdout": "ok"},
|
|
{"job_id": "2", "status": "failed", "stderr": "error"},
|
|
{"job_id": "3", "status": "timeout"},
|
|
]
|
|
|
|
with patch("un_async._make_request", new_callable=AsyncMock) as mock_req:
|
|
mock_req.side_effect = responses
|
|
|
|
results = await asyncio.gather(
|
|
execute_code("python", "print(1)"),
|
|
execute_code("python", "raise Exception()"),
|
|
execute_code("python", "while True: pass"),
|
|
)
|
|
|
|
assert results[0]["status"] == "completed"
|
|
assert results[1]["status"] == "failed"
|
|
assert results[2]["status"] == "timeout"
|
|
|
|
|
|
class TestAsyncSignature:
|
|
"""Test that functions have async signatures."""
|
|
|
|
def test_functions_are_coroutines(self):
|
|
"""Verify key functions are async."""
|
|
import inspect
|
|
|
|
# These should be coroutine functions
|
|
assert inspect.iscoroutinefunction(execute_code)
|
|
assert inspect.iscoroutinefunction(execute_async)
|
|
assert inspect.iscoroutinefunction(get_job)
|
|
assert inspect.iscoroutinefunction(wait_for_job)
|
|
assert inspect.iscoroutinefunction(cancel_job)
|
|
assert inspect.iscoroutinefunction(list_jobs)
|
|
|
|
# These should NOT be coroutine functions (sync helpers)
|
|
assert not inspect.iscoroutinefunction(un_async.detect_language)
|
|
assert not inspect.iscoroutinefunction(un_async._resolve_credentials)
|