use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use camel_cli::commands::test::document::parse_test_document;
use camel_cli::commands::test::run_tests;
use camel_cli::commands::test::runner::run_test_doc;
mod common;
use common::{drain_to_buffer, send_term, spawn_camel_run, wait_exit_bounded, wait_for_marker};
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("camel-test-beans-{tag}-{}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp dir"); dir
}
#[tokio::test(flavor = "multi_thread")]
async fn bean_route_without_registry_fails_today() {
let dir = temp_dir("no-registry");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: enricher
method: enrich
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
let err = result.doc_error.expect("doc_error must be Some"); assert!(
err.contains("Bean not found: enricher"),
"doc_error must name the missing bean, got: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn setbody_stub_transforms_body() {
let dir = temp_dir("setbody");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: enricher
method: enrich
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
enricher:
kind: setBody
config:
body: stubbed
expects:
mock:out:
count: 1
bodies: ["stubbed"]
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), 1);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn echo_stub_passes_through() {
let dir = temp_dir("echo");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: gate
method: whatever
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
gate:
kind: echo
expects:
mock:out:
count: 1
bodies: ["x"]
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), 1);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn fail_stub_surfaces_doc_error() {
let dir = temp_dir("fail-configured");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: gate
method: check
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
gate:
kind: fail
config:
message: boom
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
let err = result.doc_error.expect("doc_error must be Some"); assert!(
err.contains("boom"),
"doc_error must carry the configured message, got: {err}"
);
assert_eq!(
result.endpoint_results.len(),
0,
"no endpoint evaluations may run"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn fail_stub_default_message() {
let dir = temp_dir("fail-default");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: gate
method: check
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
gate:
kind: fail
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
let err = result.doc_error.expect("doc_error must be Some"); assert!(
err.contains("fail bean gate"),
"doc_error must carry the default message, got: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn undeclared_method_rejected_before_boot() {
let dir = temp_dir("undeclared-method");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: enricher
method: transform
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
enricher:
kind: echo
methods: [enrich]
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
let err = result.doc_error.expect("doc_error must be Some"); assert!(
err.contains("method transform is not declared"),
"doc_error must name the undeclared method, got: {err}"
);
assert!(
!err.contains("Bean not found"),
"cross-validation must fire before boot, got: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn wildcard_accepts_route_methods() {
let dir = temp_dir("wildcard");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: gate
method: m1
- bean:
name: gate
method: m2
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
gate:
kind: echo
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), 1);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn multiple_beans_one_document() {
let dir = temp_dir("multi-beans");
let yaml = r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: a
method: m1
- bean:
name: b
method: m2
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
a:
kind: setBody
config:
body: first
b:
kind: echo
expects:
mock:out:
count: 1
bodies: ["first"]
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), 1);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[test]
fn blank_bean_name_exit_2() {
let dir = temp_dir("blank-name");
let doc = dir.join("blank.test.yaml");
fs::write(
&doc,
r#"
routes:
- id: r1
from: "direct:start"
steps:
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
" ":
kind: echo
expects:
mock:out:
count: 1
"#,
)
.expect("write blank.test.yaml");
let output = Command::new(env!("CARGO_BIN_EXE_camel"))
.arg("test")
.arg(&doc)
.output()
.expect("spawn camel test"); let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(2),
"blank bean name must exit 2; stdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.contains("non-blank"),
"stderr must state the non-blank requirement; stderr:\n{stderr}"
);
}
#[test]
fn input_delivery_failure_skips_evaluation() {
let dir = temp_dir("input-fail");
let doc = dir.join("fail.test.yaml");
fs::write(
&doc,
r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: gate
method: check
- to: "mock:out"
inputs:
- to: "direct:start"
body: "x"
beans:
gate:
kind: fail
config:
message: boom
expects:
mock:out:
count: 1
"#,
)
.expect("write fail.test.yaml");
let output = Command::new(env!("CARGO_BIN_EXE_camel"))
.arg("test")
.arg(&doc)
.output()
.expect("spawn camel test"); let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(2),
"input delivery failure must exit 2; stdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stderr.contains("boom"),
"stderr must carry the fail message; stderr:\n{stderr}"
);
for line in stdout.lines() {
assert!(
!(line.starts_with("PASS") || line.starts_with("FAIL")),
"no endpoint line may be printed for the failed doc; got: {line}\nstdout:\n{stdout}"
);
}
}
#[test]
fn camel_run_ignores_beans_block() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("Camel.toml"),
r#"[default]
routes = ["config/*.yaml"]
log_level = "INFO"
"#,
)
.expect("write Camel.toml"); let config_dir = dir.path().join("config");
std::fs::create_dir_all(&config_dir).expect("create config/"); std::fs::write(
config_dir.join("routes.yaml"),
r#"routes:
- id: "demo"
from: "direct:start"
steps:
- to: "log:done"
"#,
)
.expect("write config/routes.yaml"); std::fs::write(
config_dir.join("probe.test.yaml"),
r#"routeFiles: [routes.yaml]
inputs: []
beans:
x:
kind: teleport
expects:
mock:result:
count: 1
"#,
)
.expect("write config/probe.test.yaml");
let mut child = spawn_camel_run(dir.path());
let out_buf: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let err_buf: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let stdout = child
.stdout
.take()
.expect("child stdout was configured as piped"); let stderr = child
.stderr
.take()
.expect("child stderr was configured as piped");
let out_thread_buf = Arc::clone(&out_buf);
let err_thread_buf = Arc::clone(&err_buf);
let out_handle = thread::spawn(move || drain_to_buffer(stdout, out_thread_buf));
let err_handle = thread::spawn(move || drain_to_buffer(stderr, err_thread_buf));
let alive = wait_for_marker(
&mut child,
&[Arc::clone(&out_buf), Arc::clone(&err_buf)],
"camel-cli: running",
Duration::from_secs(30),
);
let captured_so_far = || {
format!(
"stdout:\n{}\nstderr:\n{}",
out_buf.lock().expect("stdout buffer lock poisoned"),
err_buf.lock().expect("stderr buffer lock poisoned")
)
};
assert!(
alive,
"camel run did not reach the running state within 30 s;\n{}",
captured_so_far()
);
thread::sleep(Duration::from_secs(1));
let status = child.try_wait().expect("try_wait after liveness window"); assert!(
status.is_none(),
"camel run died after startup (status: {status:?}); the run must ignore \
the colocated test document;\n{}",
captured_so_far()
);
send_term(&child);
let exited = wait_exit_bounded(&mut child, Duration::from_secs(10));
let _ = out_handle.join();
let _ = err_handle.join();
assert!(
exited,
"camel run did not exit within 10 s after SIGTERM;\n{}",
captured_so_far()
);
let stdout_text = out_buf.lock().expect("stdout buffer lock poisoned").clone(); let stderr_text = err_buf.lock().expect("stderr buffer lock poisoned").clone(); for (stream, text) in [("stdout", &stdout_text), ("stderr", &stderr_text)] {
assert!(
!text.contains("probe.test.yaml"),
"{stream} names the test document; camel run must skip *.test.yaml:\n{text}"
);
assert!(
!text.contains("teleport"),
"{stream} names the invalid bean kind from the test document; camel \
run must not read it:\n{text}"
);
assert!(
!text.contains("unknown variant"),
"{stream} carries a bean validation error; camel run must not parse \
test documents:\n{text}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn intercepts_and_beans_compose() {
let dir = temp_dir("compose");
let yaml = r#"
routes:
- id: a
from: "direct:a"
steps:
- to: "kafka:orders"
- id: b
from: "direct:b"
steps:
- bean:
name: gate
method: mark
- to: "mock:out"
inputs:
- to: "direct:a"
body: "k"
- to: "direct:b"
body: "z"
intercepts:
kafka:orders:
skipTo: mock:orders
beans:
gate:
kind: setBody
config:
body: stamped
expects:
mock:orders:
count: 1
bodies: ["k"]
mock:out:
count: 1
bodies: ["stamped"]
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), 2);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn multi_doc_with_beans_isolated() {
let dir = temp_dir("multi-isolated");
let a_path = dir.join("a.test.yaml");
let b_path = dir.join("b.test.yaml");
fs::write(
&a_path,
r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: a1
method: mark
- to: "mock:oa"
inputs:
- to: "direct:start"
body: "x"
beans:
a1:
kind: setBody
config:
body: aa
expects:
mock:oa:
count: 1
bodies: ["aa"]
"#,
)
.expect("write a.test.yaml"); fs::write(
&b_path,
r#"
routes:
- id: r1
from: "direct:start"
steps:
- to: "mock:ob"
inputs:
- to: "direct:start"
body: "y"
expects:
mock:ob:
count: 1
bodies: ["y"]
"#,
)
.expect("write b.test.yaml");
let mut out = Vec::new();
let mut err = Vec::new();
let summary = run_tests(&[a_path.clone(), b_path.clone()], &mut out, &mut err).await;
let out_str = String::from_utf8(out).expect("out utf8"); let err_str = String::from_utf8(err).expect("err utf8"); assert!(
err_str.is_empty(),
"no doc_error expected; err was: {err_str}\nout was: {out_str}"
);
assert_eq!(
summary.exit_code, 0,
"both docs must pass (exit 0); err: {err_str} out: {out_str}"
);
assert_eq!(
summary.passed, 2,
"both endpoints must pass; out: {out_str} err: {err_str}"
);
assert_eq!(
summary.failed, 0,
"no failures expected; out: {out_str} err: {err_str}"
);
assert!(
out_str.contains("a.test.yaml#oa"),
"out must contain PASS for a.test.yaml mock:oa; out: {out_str}"
);
assert!(
out_str.contains("b.test.yaml#ob"),
"out must contain PASS for b.test.yaml mock:ob; out: {out_str}"
);
assert!(
out_str.contains("2 passed, 0 failed"),
"out summary must be 2 passed, 0 failed; out: {out_str}"
);
}