use proptest::prelude::*;
use serde_json::json;
use adk_code::CodeError;
use adk_code::diagnostics::parse_diagnostics;
fn make_diagnostic_json(level: &str, message: &str, code: Option<&str>) -> String {
let code_json = match code {
Some(c) => format!(r#"{{"code":"{c}","explanation":null}}"#),
None => "null".to_string(),
};
format!(
r#"{{"message":"{message}","code":{code_json},"level":"{level}","spans":[],"children":[],"rendered":"{level}: {message}"}}"#
)
}
fn arb_error_diagnostic() -> impl Strategy<Value = String> {
(
"[a-z ]{3,30}", prop::option::of("E[0-9]{4}"), )
.prop_map(|(msg, code)| make_diagnostic_json("error", &msg, code.as_deref()))
}
fn arb_warning_diagnostic() -> impl Strategy<Value = String> {
"[a-z ]{3,30}".prop_map(|msg| make_diagnostic_json("warning", &msg, None))
}
fn arb_diagnostic_stderr() -> impl Strategy<Value = String> {
prop::collection::vec(prop_oneof![arb_error_diagnostic(), arb_warning_diagnostic()], 1..=5)
.prop_map(|lines| lines.join("\n"))
}
fn arb_code_error() -> impl Strategy<Value = CodeError> {
prop_oneof![
("[a-z ]{3,30}", prop::option::of("E[0-9]{4}"),).prop_map(|(msg, code)| {
let diag = adk_code::RustDiagnostic {
level: "error".to_string(),
message: msg.clone(),
spans: vec![],
code,
};
CodeError::CompileError { diagnostics: vec![diag], stderr: format!("error: {msg}") }
}),
"[a-z_]{3,15}".prop_map(|name| CodeError::DependencyNotFound {
name,
searched: vec!["config: /fake".to_string()],
}),
(1u64..=300).prop_map(|secs| {
CodeError::Sandbox(adk_sandbox::SandboxError::Timeout {
timeout: std::time::Duration::from_secs(secs),
})
}),
(1u32..=1024).prop_map(|mb| {
CodeError::Sandbox(adk_sandbox::SandboxError::MemoryExceeded { limit_mb: mb })
}),
"[a-z ]{3,30}".prop_map(|msg| {
CodeError::Sandbox(adk_sandbox::SandboxError::ExecutionFailed(msg))
}),
"[a-z ]{3,30}"
.prop_map(|msg| { CodeError::Sandbox(adk_sandbox::SandboxError::InvalidRequest(msg)) }),
"[a-z ]{3,30}".prop_map(|msg| {
CodeError::Sandbox(adk_sandbox::SandboxError::BackendUnavailable(msg))
}),
"[a-z ]{3,30}".prop_map(CodeError::InvalidCode),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_diagnostic_parsing(stderr in arb_diagnostic_stderr()) {
let diagnostics = parse_diagnostics(&stderr);
prop_assert!(
!diagnostics.is_empty(),
"expected non-empty diagnostics for input:\n{stderr}"
);
for diag in &diagnostics {
prop_assert!(
!diag.level.is_empty(),
"diagnostic level should not be empty"
);
prop_assert!(
!diag.message.is_empty(),
"diagnostic message should not be empty"
);
prop_assert!(
matches!(diag.level.as_str(), "error" | "warning" | "note" | "help"),
"unexpected diagnostic level: {}",
diag.level
);
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_error_as_information(err in arb_code_error()) {
let json = code_error_to_json(&err);
prop_assert!(json.is_object(), "expected JSON object, got: {json}");
prop_assert!(
json.get("status").is_some(),
"missing 'status' field in: {json}"
);
let status = json["status"].as_str().unwrap();
prop_assert!(
!status.is_empty(),
"status field should not be empty"
);
let has_stderr = json.get("stderr").is_some();
let has_diagnostics = json.get("diagnostics").is_some();
prop_assert!(
has_stderr || has_diagnostics,
"expected 'stderr' or 'diagnostics' field in: {json}"
);
prop_assert!(
matches!(status, "compile_error" | "error" | "timeout" | "memory_exceeded"),
"unexpected status: {status}"
);
}
}
#[test]
fn prop_deprecated_alias_compilation() {
assert!(!std::any::type_name::<adk_code::CodeTool>().is_empty());
assert!(!std::any::type_name::<adk_code::RustExecutor>().is_empty());
assert!(!std::any::type_name::<adk_code::RustExecutorConfig>().is_empty());
assert!(!std::any::type_name::<adk_code::CodeError>().is_empty());
assert!(!std::any::type_name::<adk_code::RustDiagnostic>().is_empty());
assert!(!std::any::type_name::<dyn adk_sandbox::SandboxBackend>().is_empty());
assert!(!std::any::type_name::<adk_sandbox::ExecRequest>().is_empty());
assert!(!std::any::type_name::<adk_sandbox::ExecResult>().is_empty());
let empty = adk_code::parse_diagnostics("");
assert!(empty.is_empty());
}
fn code_error_to_json(err: &CodeError) -> serde_json::Value {
match err {
CodeError::CompileError { diagnostics, stderr } => {
let diag_json: Vec<serde_json::Value> = diagnostics
.iter()
.map(|d| {
json!({
"level": d.level,
"message": d.message,
"spans": d.spans.iter().map(|s| json!({
"file_name": s.file_name,
"line_start": s.line_start,
"line_end": s.line_end,
"column_start": s.column_start,
"column_end": s.column_end,
})).collect::<Vec<_>>(),
"code": d.code,
})
})
.collect();
json!({
"status": "compile_error",
"diagnostics": diag_json,
"stderr": stderr,
})
}
CodeError::DependencyNotFound { name, searched } => json!({
"status": "error",
"stderr": format!("dependency not found: {name} (searched: {searched:?})"),
}),
CodeError::Sandbox(sandbox_err) => {
use adk_sandbox::SandboxError;
match sandbox_err {
SandboxError::Timeout { timeout } => json!({
"status": "timeout",
"stderr": format!("execution timed out after {timeout:?}"),
"duration_ms": timeout.as_millis() as u64,
}),
SandboxError::MemoryExceeded { limit_mb } => json!({
"status": "memory_exceeded",
"stderr": format!("memory limit exceeded: {limit_mb} MB"),
}),
SandboxError::ExecutionFailed(msg) => json!({
"status": "error",
"stderr": msg,
}),
SandboxError::InvalidRequest(msg) => json!({
"status": "error",
"stderr": msg,
}),
SandboxError::BackendUnavailable(msg) => json!({
"status": "error",
"stderr": msg,
}),
}
}
CodeError::InvalidCode(msg) => json!({
"status": "error",
"stderr": msg,
}),
}
}