use std::collections::HashMap;
use std::sync::Arc;
use base64::Engine;
use camel_api::{Body, Exchange};
use camel_component_api::ComponentBundle;
use camel_component_exec::{
ExecBundle, ExecProducer, ExecResult,
headers::{
CAMEL_EXEC_ARGS, CAMEL_EXEC_EXIT_ACCEPTED, CAMEL_EXEC_EXIT_CODE, CAMEL_EXEC_TIMED_OUT,
},
};
use tokio::sync::Semaphore;
use tower::Service;
fn make_cfg(profiles_toml: &str) -> toml::Value {
let s = format!("workspace_root = \".\"\n{profiles_toml}");
toml::from_str(&s).unwrap()
}
fn result_from_body(ex: &Exchange) -> ExecResult {
let v = match &ex.input.body {
Body::Json(v) => v.clone(),
other => panic!("expected Body::Json, got {other:?}"),
};
serde_json::from_value(v).expect("deserialize ExecResult")
}
fn producer_from_bundle(bundle: &ExecBundle, profile_name: &str) -> ExecProducer {
let config = bundle.config();
let profile = config
.profile(profile_name)
.unwrap_or_else(|| panic!("profile {profile_name:?} not found"))
.clone();
ExecProducer::new(
Arc::new(profile),
Arc::new(config.clone()),
"integration-test".into(),
HashMap::new(),
Arc::new(Semaphore::new(1)),
None,
)
}
fn set_args(ex: &mut Exchange, args: &[&str]) {
let v: Vec<String> = args.iter().map(|s| s.to_string()).collect();
ex.input.headers.insert(
CAMEL_EXEC_ARGS.to_string(),
serde_json::to_value(v).unwrap(),
);
}
#[tokio::test]
async fn producer_echo_via_bundle() {
let bundle = ExecBundle::from_toml(make_cfg(
r#"
[[profiles]]
name = "echo"
executable = "echo"
args = { allow = "any" }
accepted_exit_codes = [0]
timeout_secs = 5
"#,
))
.unwrap();
let mut producer = producer_from_bundle(&bundle, "echo");
let mut ex = Exchange::default();
set_args(&mut ex, &["hello"]);
let exchange = Service::call(&mut producer, ex)
.await
.expect("echo must succeed");
let result = result_from_body(&exchange);
assert_eq!(result.exit_code, Some(0));
assert!(!result.timed_out);
let stdout_bytes = base64::engine::general_purpose::STANDARD
.decode(&result.stdout)
.unwrap();
assert_eq!(stdout_bytes, b"hello\n");
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_EXIT_ACCEPTED),
Some(&serde_json::Value::Bool(true))
);
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_TIMED_OUT),
Some(&serde_json::Value::Bool(false))
);
}
#[tokio::test]
async fn producer_non_zero_exit_via_bundle() {
let bundle = ExecBundle::from_toml(make_cfg(
r#"
[[profiles]]
name = "sh"
executable = "sh"
args = { allow = "any" }
allow_shell = true
accepted_exit_codes = [0]
timeout_secs = 5
"#,
))
.unwrap();
let mut producer = producer_from_bundle(&bundle, "sh");
let mut ex = Exchange::default();
set_args(&mut ex, &["-c", "exit 2"]);
let exchange = Service::call(&mut producer, ex)
.await
.expect("non-zero exit must be non-error");
let result = result_from_body(&exchange);
assert_eq!(result.exit_code, Some(2));
assert!(!result.timed_out);
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_EXIT_ACCEPTED),
Some(&serde_json::Value::Bool(false))
);
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_EXIT_CODE),
Some(&serde_json::Value::from(2))
);
}
#[tokio::test]
async fn producer_timeout_tree_kill_via_bundle() {
let bundle = ExecBundle::from_toml(make_cfg(
r#"
[[profiles]]
name = "sleep"
executable = "sleep"
args = { allow = "any" }
accepted_exit_codes = [0]
timeout_secs = 1
"#,
))
.unwrap();
let mut producer = producer_from_bundle(&bundle, "sleep");
let mut ex = Exchange::default();
set_args(&mut ex, &["30"]);
let deadline = tokio::time::Duration::from_secs(5);
let exchange = tokio::time::timeout(deadline, Service::call(&mut producer, ex))
.await
.expect("timeout must complete within 5s (tree-kill test)")
.expect("timeout must be non-error outcome");
let result = result_from_body(&exchange);
assert_eq!(result.exit_code, None, "killed process has no exit code");
assert!(result.timed_out, "must be marked timed out");
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_EXIT_ACCEPTED),
Some(&serde_json::Value::Bool(false))
);
assert_eq!(
exchange.input.headers.get(CAMEL_EXEC_TIMED_OUT),
Some(&serde_json::Value::Bool(true))
);
assert!(
!exchange.input.headers.contains_key(CAMEL_EXEC_EXIT_CODE),
"no exit code header on killed process"
);
}