feat: full feature parity for all 42 SDKs + comprehensive tests
All SDKs now implement 58+ functions matching C reference (un.h): - Execution (8): execute, execute_async, wait_job, get_job, cancel_job, list_jobs, get_languages, detect_language - Sessions (9): list, get, create, destroy, freeze, unfreeze, boost, unboost, execute - Services (17): list, get, create, destroy, freeze, unfreeze, lock, unlock, set_unfreeze_on_demand, redeploy, logs, execute, env_get/set/delete/export, resize - Snapshots (9): list, get, session, service, restore, delete, lock, unlock, clone - Images (13): list, get, publish, delete, lock, unlock, set_visibility, grant/revoke_access, list_trusted, transfer, spawn, clone - PaaS Logs (2): fetch, stream - Utilities (5): validate_keys, hmac_sign, health_check, version, last_error Test suites created for all SDKs with unit, integration, and functional tests. Languages: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart, .NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP, PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl, TypeScript, V, Zig
This commit is contained in:
parent
a5155aed4f
commit
6e746ace44
90 changed files with 28955 additions and 3028 deletions
|
|
@ -1302,6 +1302,19 @@ pub fn list_snapshots(creds: &Credentials) -> Result<Vec<Snapshot>> {
|
|||
Ok(response.snapshots)
|
||||
}
|
||||
|
||||
/// Get details of a specific snapshot.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `snapshot_id` - Snapshot ID to retrieve
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Snapshot information
|
||||
pub fn get_snapshot(snapshot_id: &str, creds: &Credentials) -> Result<Snapshot> {
|
||||
let path = format!("/snapshots/{}", snapshot_id);
|
||||
make_request("GET", &path, creds, None::<&()>)
|
||||
}
|
||||
|
||||
/// Restore a snapshot to create a new session or service.
|
||||
///
|
||||
/// # Arguments
|
||||
|
|
@ -2265,6 +2278,30 @@ pub fn redeploy_service(service_id: &str, creds: &Credentials) -> Result<Service
|
|||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Resize a service by changing its vCPU count.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `service_id` - Service ID to resize
|
||||
/// * `vcpu` - New vCPU count (1-8)
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Updated Service information
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let service = resize_service("service-123", 4, &creds)?;
|
||||
/// println!("Service now has {} vCPUs", service.vcpu);
|
||||
/// ```
|
||||
pub fn resize_service(service_id: &str, vcpu: u32, creds: &Credentials) -> Result<Service> {
|
||||
let path = format!("/services/{}/resize", service_id);
|
||||
let body = serde_json::json!({
|
||||
"vcpu": vcpu
|
||||
});
|
||||
make_request("POST", &path, creds, Some(&body))
|
||||
}
|
||||
|
||||
/// Execute a command in a service container.
|
||||
///
|
||||
/// # Arguments
|
||||
|
|
@ -2397,6 +2434,278 @@ pub fn image(prompt: &str, creds: &Credentials, opts: Option<ImageOptions>) -> R
|
|||
make_request("POST", "/image", creds, Some(&payload))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PaaS Logs API Functions
|
||||
// =============================================================================
|
||||
|
||||
/// Options for fetching logs.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LogsFetchOptions {
|
||||
/// Number of lines to fetch (1-10000)
|
||||
pub lines: Option<u32>,
|
||||
/// Time window ("1m", "5m", "1h", "1d")
|
||||
pub since: Option<String>,
|
||||
/// Optional filter pattern
|
||||
pub grep: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from logs fetch.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LogsEntry {
|
||||
/// Log source
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
/// Log line content
|
||||
#[serde(default)]
|
||||
pub line: String,
|
||||
/// Timestamp
|
||||
#[serde(default)]
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Fetch batch logs from the portal.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
/// * `opts` - Fetch options (can be None for defaults)
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of log entries
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// // Fetch last 100 lines from all sources
|
||||
/// let opts = LogsFetchOptions {
|
||||
/// lines: Some(100),
|
||||
/// since: Some("1h".to_string()),
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
/// let logs = logs_fetch("all", Some(opts), &creds)?;
|
||||
/// for entry in logs {
|
||||
/// println!("[{}] {}", entry.source, entry.line);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn logs_fetch(source: &str, opts: Option<LogsFetchOptions>, creds: &Credentials) -> Result<Vec<LogsEntry>> {
|
||||
let mut path = String::from("/paas/logs");
|
||||
let mut params = vec![];
|
||||
|
||||
if !source.is_empty() {
|
||||
params.push(format!("source={}", source));
|
||||
}
|
||||
|
||||
if let Some(opts) = opts {
|
||||
if let Some(lines) = opts.lines {
|
||||
params.push(format!("lines={}", lines));
|
||||
}
|
||||
if let Some(since) = opts.since {
|
||||
params.push(format!("since={}", since));
|
||||
}
|
||||
if let Some(grep) = opts.grep {
|
||||
params.push(format!("grep={}", grep));
|
||||
}
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
path = format!("{}?{}", path, params.join("&"));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LogsResponse {
|
||||
#[serde(default)]
|
||||
logs: Vec<LogsEntry>,
|
||||
}
|
||||
|
||||
let response: LogsResponse = make_request("GET", &path, creds, None::<&()>)?;
|
||||
Ok(response.logs)
|
||||
}
|
||||
|
||||
/// Callback function for streaming logs.
|
||||
pub type LogCallback = fn(source: &str, line: &str);
|
||||
|
||||
/// Stream logs via Server-Sent Events.
|
||||
///
|
||||
/// This function blocks until the stream is closed or an error occurs.
|
||||
/// Note: This is a simplified implementation that reads until EOF.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `source` - Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
|
||||
/// * `grep` - Optional filter pattern (empty string for no filter)
|
||||
/// * `callback` - Function called for each log line
|
||||
/// * `creds` - API credentials
|
||||
///
|
||||
/// # Returns
|
||||
/// Ok(()) on clean shutdown, error on failure
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
///
|
||||
/// fn handle_log(source: &str, line: &str) {
|
||||
/// println!("[{}] {}", source, line);
|
||||
/// }
|
||||
///
|
||||
/// logs_stream("all", "", handle_log, &creds)?;
|
||||
/// ```
|
||||
pub fn logs_stream(source: &str, grep: &str, callback: LogCallback, creds: &Credentials) -> Result<()> {
|
||||
let mut path = String::from("/paas/logs/stream");
|
||||
let mut params = vec![];
|
||||
|
||||
if !source.is_empty() {
|
||||
params.push(format!("source={}", source));
|
||||
}
|
||||
if !grep.is_empty() {
|
||||
params.push(format!("grep={}", grep));
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
path = format!("{}?{}", path, params.join("&"));
|
||||
}
|
||||
|
||||
let url = format!("{}{}", API_BASE, path);
|
||||
let timestamp = get_timestamp();
|
||||
let signature = sign_request(&creds.secret_key, timestamp, "GET", &path, "");
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(None) // No timeout for streaming
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", creds.public_key))
|
||||
.header("X-Timestamp", timestamp.to_string())
|
||||
.header("X-Signature", signature)
|
||||
.header("Accept", "text/event-stream")
|
||||
.header("User-Agent", "un-rust-sync/2.0")
|
||||
.send()?;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
if status < 200 || status >= 300 {
|
||||
let response_text = response.text()?;
|
||||
return Err(UnsandboxError::ApiError {
|
||||
status,
|
||||
message: response_text,
|
||||
});
|
||||
}
|
||||
|
||||
let reader = BufReader::new(response);
|
||||
let mut current_source = source.to_string();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse SSE format
|
||||
if let Some(event) = line.strip_prefix("event:") {
|
||||
current_source = event.trim().to_string();
|
||||
} else if let Some(data) = line.strip_prefix("data:") {
|
||||
let data = data.trim();
|
||||
if !data.is_empty() {
|
||||
callback(¤t_source, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
/// SDK version string
|
||||
pub const SDK_VERSION: &str = "4.2.0";
|
||||
|
||||
/// Compute HMAC-SHA256 signature for a message.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `secret_key` - Secret key for signing
|
||||
/// * `message` - Message to sign
|
||||
///
|
||||
/// # Returns
|
||||
/// Lowercase hex-encoded signature string
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let sig = hmac_sign("my-secret", "hello");
|
||||
/// println!("Signature: {}", sig);
|
||||
/// ```
|
||||
pub fn hmac_sign(secret_key: &str, message: &str) -> String {
|
||||
let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(message.as_bytes());
|
||||
let result = mac.finalize();
|
||||
hex::encode(result.into_bytes())
|
||||
}
|
||||
|
||||
/// Check if the API is reachable and responding.
|
||||
///
|
||||
/// # Returns
|
||||
/// true if healthy, false otherwise
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// if health_check() {
|
||||
/// println!("API is healthy");
|
||||
/// } else {
|
||||
/// println!("API is unreachable");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn health_check() -> bool {
|
||||
let client = match reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match client.get(format!("{}/health", API_BASE)).send() {
|
||||
Ok(resp) => resp.status().as_u16() == 200,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the SDK version string.
|
||||
///
|
||||
/// # Returns
|
||||
/// Version string (e.g., "4.2.0")
|
||||
pub fn version() -> &'static str {
|
||||
SDK_VERSION
|
||||
}
|
||||
|
||||
/// Thread-local storage for the last error message.
|
||||
thread_local! {
|
||||
static LAST_ERROR: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||
}
|
||||
|
||||
/// Set the last error message (internal use).
|
||||
pub fn set_last_error(msg: &str) {
|
||||
LAST_ERROR.with(|e| {
|
||||
*e.borrow_mut() = msg.to_string();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the last error message from the SDK.
|
||||
///
|
||||
/// # Returns
|
||||
/// Last error message, or empty string if none
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// if let Err(_) = some_operation() {
|
||||
/// println!("Error: {}", last_error());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn last_error() -> String {
|
||||
LAST_ERROR.with(|e| e.borrow().clone())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLI Exit Codes
|
||||
// =============================================================================
|
||||
|
|
|
|||
211
clients/rust/sync/tests/lib_test.rs
Normal file
211
clients/rust/sync/tests/lib_test.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Tests for the Rust unsandbox SDK
|
||||
// Run with: cargo test
|
||||
|
||||
use un::*;
|
||||
use std::env;
|
||||
|
||||
// ============================================================================
|
||||
// Unit Tests - Test exported library functions
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_detect_language() {
|
||||
assert_eq!(detect_language("script.py"), Some("python"));
|
||||
assert_eq!(detect_language("script.js"), Some("javascript"));
|
||||
assert_eq!(detect_language("script.ts"), Some("typescript"));
|
||||
assert_eq!(detect_language("script.rb"), Some("ruby"));
|
||||
assert_eq!(detect_language("script.go"), Some("go"));
|
||||
assert_eq!(detect_language("script.rs"), Some("rust"));
|
||||
assert_eq!(detect_language("script.c"), Some("c"));
|
||||
assert_eq!(detect_language("script.cpp"), Some("cpp"));
|
||||
assert_eq!(detect_language("script.d"), Some("d"));
|
||||
assert_eq!(detect_language("script.zig"), Some("zig"));
|
||||
assert_eq!(detect_language("script.sh"), Some("bash"));
|
||||
assert_eq!(detect_language("script.lua"), Some("lua"));
|
||||
assert_eq!(detect_language("script.php"), Some("php"));
|
||||
assert_eq!(detect_language("script.unknown"), None);
|
||||
assert_eq!(detect_language("script"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hmac_sign() {
|
||||
let secret_key = "test-secret";
|
||||
let message = "test-message";
|
||||
|
||||
let result = hmac_sign(secret_key, message);
|
||||
|
||||
// Should return a 64-character hex string
|
||||
assert_eq!(result.len(), 64, "HMAC signature should be 64 characters");
|
||||
|
||||
// Should be deterministic
|
||||
let result2 = hmac_sign(secret_key, message);
|
||||
assert_eq!(result, result2, "HMAC sign is not deterministic");
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
let result3 = hmac_sign(secret_key, "different-message");
|
||||
assert_ne!(result, result3, "Different inputs should produce different signatures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let v = version();
|
||||
assert!(!v.is_empty(), "Version should not be empty");
|
||||
// Should be in semver format (at least "0.0.0")
|
||||
assert!(v.len() >= 5, "Version should be in semver format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_error() {
|
||||
// Set an error
|
||||
set_last_error("test error message");
|
||||
|
||||
// Retrieve it
|
||||
let err = last_error();
|
||||
assert_eq!(err, "test error message");
|
||||
|
||||
// Clear it
|
||||
set_last_error("");
|
||||
let err = last_error();
|
||||
assert!(err.is_empty(), "Error should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_new() {
|
||||
let pk = "unsb-pk-test-test-test-test";
|
||||
let sk = "unsb-sk-test1-test2-test3-test4";
|
||||
|
||||
let creds = Credentials::new(pk, sk);
|
||||
|
||||
assert_eq!(creds.public_key, pk);
|
||||
assert_eq!(creds.secret_key, sk);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Test SDK internal consistency
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_resolve_credentials_from_env() {
|
||||
// Save original env vars
|
||||
let orig_pk = env::var("UNSANDBOX_PUBLIC_KEY").ok();
|
||||
let orig_sk = env::var("UNSANDBOX_SECRET_KEY").ok();
|
||||
|
||||
// Set test env vars
|
||||
let test_pk = "unsb-pk-test-test-test-test";
|
||||
let test_sk = "unsb-sk-test1-test2-test3-test4";
|
||||
env::set_var("UNSANDBOX_PUBLIC_KEY", test_pk);
|
||||
env::set_var("UNSANDBOX_SECRET_KEY", test_sk);
|
||||
|
||||
// Test
|
||||
let creds = resolve_credentials(None, None).expect("Should resolve from env");
|
||||
assert_eq!(creds.public_key, test_pk);
|
||||
assert_eq!(creds.secret_key, test_sk);
|
||||
|
||||
// Restore original env vars
|
||||
match orig_pk {
|
||||
Some(v) => env::set_var("UNSANDBOX_PUBLIC_KEY", v),
|
||||
None => env::remove_var("UNSANDBOX_PUBLIC_KEY"),
|
||||
}
|
||||
match orig_sk {
|
||||
Some(v) => env::set_var("UNSANDBOX_SECRET_KEY", v),
|
||||
None => env::remove_var("UNSANDBOX_SECRET_KEY"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_credentials_from_args() {
|
||||
let test_pk = "unsb-pk-arg1-arg2-arg3-arg4";
|
||||
let test_sk = "unsb-sk-arg11-arg22-arg33-arg44";
|
||||
|
||||
let creds = resolve_credentials(Some(test_pk), Some(test_sk))
|
||||
.expect("Should resolve from args");
|
||||
assert_eq!(creds.public_key, test_pk);
|
||||
assert_eq!(creds.secret_key, test_sk);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Functional Tests - Test against real API (requires credentials)
|
||||
// ============================================================================
|
||||
|
||||
fn get_test_credentials() -> Option<Credentials> {
|
||||
resolve_credentials(None, None).ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_health_check() {
|
||||
let healthy = health_check();
|
||||
if !healthy {
|
||||
eprintln!("API health check returned unhealthy (API may be unreachable)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_get_languages() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let languages = get_languages(&creds).expect("GetLanguages failed");
|
||||
|
||||
assert!(!languages.is_empty(), "Languages list should not be empty");
|
||||
|
||||
// Should include common languages
|
||||
assert!(
|
||||
languages.contains(&"python".to_string()),
|
||||
"Languages should include python"
|
||||
);
|
||||
assert!(
|
||||
languages.contains(&"javascript".to_string()),
|
||||
"Languages should include javascript"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_validate_keys() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let result = validate_keys(&creds).expect("ValidateKeys failed");
|
||||
|
||||
assert!(result.valid, "Keys should be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_execute_code() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let result = execute_code("python", "print('hello from rust test')", &creds)
|
||||
.expect("ExecuteCode failed");
|
||||
|
||||
assert!(!result.output.is_empty(), "Output should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_sessions() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _sessions = list_sessions(&creds).expect("ListSessions failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_services() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _services = list_services(&creds).expect("ListServices failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_snapshots() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _snapshots = list_snapshots(&creds).expect("ListSnapshots failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Run with: cargo test -- --ignored
|
||||
fn test_list_images() {
|
||||
let creds = get_test_credentials().expect("No credentials for functional test");
|
||||
let _images = list_images(None, &creds).expect("ListImages failed");
|
||||
// Should return a list (possibly empty)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue