use std::io::Read;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
fn drain_to_buffer<R: Read + Send + 'static>(mut reader: R, buffer: Arc<Mutex<String>>) {
let mut chunk = [0u8; 4096];
loop {
match reader.read(&mut chunk) {
Ok(0) => return,
Ok(n) => {
let text = String::from_utf8_lossy(&chunk[..n]);
let mut guard = buffer.lock().expect("buffer lock poisoned");
guard.push_str(&text);
}
Err(e) => {
eprintln!("reader thread io error: {e}");
return;
}
}
}
}
fn spawn_camel_run(dir: &Path) -> Child {
let config_path = dir.join("Camel.toml");
Command::new(env!("CARGO_BIN_EXE_camel"))
.arg("run")
.arg("--no-watch")
.arg("--config")
.arg(&config_path)
.current_dir(dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.spawn()
.expect("failed to spawn `camel` binary")
}
fn run_observe_then_signal(dir: &Path, observe: &str, timeout: Duration) -> (i32, String, bool) {
let mut child = spawn_camel_run(dir);
let buffer: 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_buf = Arc::clone(&buffer);
let err_buf = Arc::clone(&buffer);
let out_handle = thread::spawn(move || drain_to_buffer(stdout, out_buf));
let err_handle = thread::spawn(move || drain_to_buffer(stderr, err_buf));
let start = Instant::now();
let step = Duration::from_millis(25);
let observed = loop {
{
let guard = buffer.lock().expect("buffer lock poisoned");
if guard.contains(observe) {
break true;
}
}
if start.elapsed() >= timeout {
break false;
}
if let Ok(Some(_)) = child.try_wait() {
break false;
}
thread::sleep(step);
};
if observed {
let kill_status = Command::new("kill")
.arg("-TERM")
.arg(child.id().to_string())
.status()
.expect("failed to spawn `kill -TERM`");
assert!(
kill_status.success(),
"`kill -TERM` returned non-zero: {kill_status:?}"
);
}
let exit_start = Instant::now();
let exit_code = loop {
match child.try_wait() {
Ok(Some(status)) => break status.code().unwrap_or(-1),
Ok(None) => {
if exit_start.elapsed() >= Duration::from_secs(10) {
let _ = child.kill();
let _ = child.wait();
break -1;
}
thread::sleep(step);
}
Err(e) => panic!("try_wait failed: {e}"),
}
};
let _ = out_handle.join();
let _ = err_handle.join();
let captured = buffer.lock().expect("buffer lock poisoned").clone();
(exit_code, captured, observed)
}
#[test]
fn empty_discovery_emits_warn_and_starts() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("Camel.toml"),
r#"[default]
routes = ["routes/*.yaml"]
log_level = "INFO"
watch = false
"#,
)
.expect("write Camel.toml");
let (exit_code, output, observed) = run_observe_then_signal(
dir.path(),
"matched zero route files",
Duration::from_secs(60),
);
assert!(
observed,
"expected a WARN containing `matched zero route files` when the \
discovery glob matches nothing; got:\n{output}"
);
assert!(
output.contains("routes/*.yaml"),
"expected the WARN to name the discovery patterns; got:\n{output}"
);
assert_eq!(
exit_code, 0,
"expected graceful shutdown (exit 0) after the WARN; got {exit_code}\n--- captured ---\n{output}\n--- end ---"
);
}
#[test]
fn wildcard_over_only_test_doc_warns_zero_routes_not_error() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join("routes")).expect("mkdir routes");
std::fs::write(
dir.path().join("Camel.toml"),
r#"[default]
routes = ["routes/*.test.yaml"]
log_level = "INFO"
watch = false
"#,
)
.expect("write Camel.toml");
std::fs::write(
dir.path().join("routes/demo.test.yaml"),
"not: [a route document",
)
.expect("write demo.test.yaml");
let (exit_code, output, observed) = run_observe_then_signal(
dir.path(),
"matched zero route files",
Duration::from_secs(60),
);
assert!(
observed,
"expected the zero-routes WARN when the glob matches only a test \
doc and the run stays alive; got:\n{output}"
);
assert!(
output.contains("routes/*.test.yaml"),
"expected the WARN to name the discovery patterns; got:\n{output}"
);
assert!(
!output.contains("demo.test.yaml"),
"a wildcard match on a test doc must be skipped silently, not \
surfaced as a discovery error naming it; got:\n{output}"
);
assert_eq!(
exit_code, 0,
"expected the run to stay alive until SIGTERM and exit gracefully (0); got {exit_code}\n--- captured ---\n{output}\n--- end ---"
);
}
#[test]
fn empty_discovery_default_path_warns_with_raw_pattern() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("Camel.toml"),
r#"[default]
log_level = "INFO"
watch = false
"#,
)
.expect("write Camel.toml");
let (exit_code, output, observed) = run_observe_then_signal(
dir.path(),
"matched zero route files",
Duration::from_secs(60),
);
assert!(
observed,
"expected a WARN containing `matched zero route files` on the default \
path; got:\n{output}"
);
assert!(
output.contains("routes/*.yaml"),
"expected the WARN to name the raw default pattern `routes/*.yaml` \
instead of the empty expanded list; got:\n{output}"
);
assert!(
!output.contains("patterns []"),
"expected the WARN NOT to print the empty expanded pattern list; got:\n{output}"
);
assert_eq!(
exit_code, 0,
"expected graceful shutdown (exit 0) after the WARN; got {exit_code}\n--- captured ---\n{output}\n--- end ---"
);
}