//! `wai serve ` against the programs it starts: the room service and the relay. //! //! - `wai serve room --config --check` prints what `wai-room --config --check` prints //! and exits as it exits, byte for byte: the configuration and the keys it names read as the //! service starts from them, and refused (exit 1) when either does not read; nothing is started; //! - the same for the relay, its example configuration and one it refuses; //! - `wai serve room --config ` starts the room service itself, which answers on the address //! its configuration names. //! //! `wai` is built from `wai/wai-rs` with the feature `mcp` into the target directory //! tests/cli_dev_room.rs builds it in, and the relay `wai-relay` from the joule-code workspace. use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use std::time::{Duration, Instant}; fn cargo() -> String { std::env::var("CARGO").ok().or(option_env!("CARGO").map(String::from)).unwrap_or_else(|| "cargo".into()) } fn exe(name: &str) -> String { if cfg!(windows) { format!("{name}.exe") } else { name.to_string() } } fn wai_bin() -> PathBuf { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../../wai/wai-rs/Cargo.toml"); let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("wai-mcp"); let status = Command::new(cargo()) .args(["build", "--locked", "--no-default-features", "--features", "mcp", "--bin", "wai", "--manifest-path"]) .arg(&manifest) .env("CARGO_TARGET_DIR", &target) .status() .expect("cargo runs"); assert!(status.success(), "the wai binary builds with the feature mcp"); target.join("debug").join(exe("wai")) } fn relay_bin() -> PathBuf { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"); let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("wai-relay"); let status = Command::new(cargo()) .args(["build", "--locked", "-p", "joule-moq-relay", "--bin", "wai-relay", "--manifest-path"]) .arg(&manifest) .env("CARGO_TARGET_DIR", &target) .status() .expect("cargo runs"); assert!(status.success(), "the relay builds"); target.join("debug").join(exe("wai-relay")) } fn run(bin: &Path, dir: &Path, args: &[&str]) -> Output { Command::new(bin).current_dir(dir).args(args).env("WAI_CONFIG_DIR", dir.join(".wai-config")).stdin(Stdio::null()).output().expect("it runs") } fn text(o: &Output) -> String { format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) } fn same(a: &Output, b: &Output, what: &str) { assert_eq!(a.status.code(), b.status.code(), "{what}: the exit status\n{}\n---\n{}", text(a), text(b)); assert_eq!(String::from_utf8_lossy(&a.stdout), String::from_utf8_lossy(&b.stdout), "{what}: standard output"); assert_eq!(String::from_utf8_lossy(&a.stderr), String::from_utf8_lossy(&b.stderr), "{what}: standard error"); } fn example(rel: &str) -> String { std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)).unwrap() } /// A port no one listens on now. fn free_port() -> u16 { std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port() } #[test] fn serve_checks_and_starts_each_program_as_itself() { let wai = wai_bin(); let relay = relay_bin(); let room = Path::new(env!("CARGO_BIN_EXE_wai-room")); let d = std::env::temp_dir().join(format!("joule-moq-room-serve-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); let keygen = run(room, &d, &["keygen", "keys.toml"]); assert!(keygen.status.success(), "{}", text(&keygen)); let port = free_port(); let cfg = example("room.example.toml").replace("keys = \"/path/to/keys.toml\"", "keys = \"keys.toml\"").replace("listen = \"127.0.0.1:9682\"", &format!("listen = \"127.0.0.1:{port}\"")); assert!(cfg.contains("keys = \"keys.toml\"") && cfg.contains(&port.to_string()), "room.example.toml names its keys and listen address as this test reads them"); std::fs::write(d.join("room.toml"), &cfg).unwrap(); std::fs::write(d.join("no-keys.toml"), cfg.replace("keys = \"keys.toml\"", "keys = \"gone.toml\"")).unwrap(); std::fs::write(d.join("not-toml.toml"), "listen = \n").unwrap(); let r = room.to_str().unwrap(); for (file, holds) in [("room.toml", true), ("no-keys.toml", false), ("not-toml.toml", false)] { let ours = run(&wai, &d, &["serve", "room", "--config", file, "--check", "--bin", r]); let theirs = run(room, &d, &["--config", file, "--check"]); same(&ours, &theirs, file); assert_eq!(theirs.status.success(), holds, "{file}: {}", text(&theirs)); } // The relay: its example, and the example with an address that is none. let rl = relay.to_str().unwrap(); std::fs::write(d.join("relay.toml"), example("../joule-moq-relay/relay.example.toml")).unwrap(); std::fs::write(d.join("bad-relay.toml"), example("../joule-moq-relay/relay.example.toml").replacen("quic_listen = \"127.0.0.1:4443\"", "quic_listen = \"nowhere\"", 1)).unwrap(); for (file, holds) in [("relay.toml", true), ("bad-relay.toml", false)] { let ours = run(&wai, &d, &["serve", "relay", "--config", file, "--check", "--bin", rl]); let theirs = run(&relay, &d, &["--config", file, "--check"]); same(&ours, &theirs, file); assert_eq!(theirs.status.success(), holds, "{file}: {}", text(&theirs)); } // Started: the room service answers where its configuration says, as the program itself. let mut child = Command::new(&wai) .current_dir(&d) .args(["serve", "room", "--config", "room.toml", "--bin", r]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("wai serve starts"); let until = Instant::now() + Duration::from_secs(20); let mut up = false; while Instant::now() < until { if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() { up = true; break; } if let Ok(Some(s)) = child.try_wait() { panic!("wai serve room stopped: {s}"); } std::thread::sleep(Duration::from_millis(100)); } let _ = child.kill(); let _ = child.wait(); assert!(up, "the room service listens on 127.0.0.1:{port}"); let _ = std::fs::remove_dir_all(&d); }