#![cfg(feature = "generate")]
use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
const DEADLINE: Duration = Duration::from_secs(20);
const BIN: &str = env!("CARGO_BIN_EXE_weida");
struct Served {
child: Child,
address: String,
stdout: Option<BufReader<std::process::ChildStdout>>,
}
impl Served {
fn start(args: &[&str]) -> Served {
let mut child = Command::new(BIN)
.arg("serve")
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the binary");
let stdout = child.stdout.take().expect("piped stdout");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let read = reader.read_line(&mut line);
let _ = tx.send(read.map(|_| (line, reader)));
});
let (address, stdout) = match rx.recv_timeout(DEADLINE) {
Ok(Ok((line, reader))) => (line.trim().to_owned(), reader),
Ok(Err(e)) => panic!("reading the served address: {e}"),
Err(_) => panic!("the binary printed no address within {DEADLINE:?}"),
};
assert!(
!address.is_empty(),
"serve must print the address a client dials"
);
Served {
child,
address,
stdout: Some(stdout),
}
}
}
impl Drop for Served {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn run(args: &[&str], payload: &[u8]) -> (i32, Vec<u8>) {
let mut child = Command::new(BIN)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the binary");
child
.stdin
.take()
.expect("piped stdin")
.write_all(payload)
.expect("write the payload");
let mut stdout = child.stdout.take().expect("piped stdout");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut out = Vec::new();
let read = stdout.read_to_end(&mut out);
let status = child.wait();
let _ = tx.send(read.and(status).map(|s| (s.code().unwrap_or(-1), out)));
});
match rx.recv_timeout(DEADLINE) {
Ok(Ok(pair)) => pair,
Ok(Err(e)) => panic!("running {args:?}: {e}"),
Err(_) => panic!("{args:?} did not finish within {DEADLINE:?}"),
}
}
fn wrong_key(address: &str) -> String {
let at = address.find('@').expect("a served address names its key");
format!("weida://sha256:{}{}", "0".repeat(64), &address[at..])
}
#[test]
fn a_request_round_trips_through_the_echo_server() {
let server = Served::start(&["--echo", "weida://127.0.0.1:0/echo"]);
let (code, out) = run(&["request", &server.address], b"hello from a test");
assert_eq!(code, 0, "stdout was {:?}", String::from_utf8_lossy(&out));
assert_eq!(out, b"hello from a test");
}
#[test]
fn the_exit_code_names_the_error_a_script_can_act_on() {
let server = Served::start(&["--echo", "weida://127.0.0.1:0/echo"]);
let unknown = server.address.replace("/echo", "/nowhere");
let (code, out) = run(&["request", &unknown], b"x");
assert_eq!(code, 4, "unknown endpoint");
assert!(out.is_empty(), "nothing may reach stdout on a failure");
let (code, out) = run(&["request", &wrong_key(&server.address)], b"x");
assert_eq!(code, 6, "untrusted key");
assert!(out.is_empty());
let (code, _) = run(&["frobnicate"], b"");
assert_eq!(code, 2, "usage");
}
#[test]
fn help_and_version_are_answers_rather_than_errors() {
let (code, out) = run(&["--version"], b"");
assert_eq!(code, 0);
let printed = String::from_utf8(out).expect("utf-8");
assert!(
printed.starts_with("weida ") && printed.contains(env!("CARGO_PKG_VERSION")),
"{printed:?}"
);
let (code, out) = run(&["--help"], b"");
assert_eq!(code, 0, "a help request is not a usage error");
let printed = String::from_utf8(out).expect("utf-8");
for verb in ["send", "request", "sub", "serve"] {
assert!(printed.contains(verb), "usage must name {verb}");
}
}
fn sunk(server: &mut Served, want: usize) -> Vec<u8> {
let mut stdout = server.stdout.take().expect("the served stdout");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut buf = vec![0u8; want];
let read = stdout.read_exact(&mut buf);
let _ = tx.send(read.map(|()| buf));
});
match rx.recv_timeout(DEADLINE) {
Ok(Ok(buf)) => buf,
Ok(Err(e)) => panic!("reading the sink's stdout: {e}"),
Err(_) => panic!("the sink printed nothing within {DEADLINE:?}"),
}
}
#[cfg(unix)]
#[test]
fn a_send_reaches_a_sink_over_a_unix_socket() {
let socket = std::env::temp_dir().join(format!("weida-cli-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&socket);
let encoded: String = socket
.to_str()
.expect("utf-8 path")
.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
(b as char).to_string()
}
other => format!("%{other:02X}"),
})
.collect();
let url = format!("weida+unix://{encoded}/sink");
let mut server = Served::start(&["--sink", &url]);
assert_eq!(server.address, url, "a local address needs no fingerprint");
let (code, _) = run(&["send", &url], b"over a unix socket");
assert_eq!(code, 0);
assert_eq!(
sunk(&mut server, b"over a unix socket".len()),
b"over a unix socket"
);
let _ = std::fs::remove_file(&socket);
}
#[cfg(windows)]
#[test]
fn a_send_reaches_a_sink_over_a_named_pipe() {
let url = format!("weida+pipe://weida-cli-{}/sink", std::process::id());
let mut server = Served::start(&["--sink", &url]);
assert_eq!(server.address, url, "a local address needs no fingerprint");
let (code, _) = run(&["send", &url], b"over a named pipe");
assert_eq!(code, 0);
assert_eq!(
sunk(&mut server, b"over a named pipe".len()),
b"over a named pipe"
);
}