#![cfg(unix)]
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
struct Dir(PathBuf);
impl Dir {
fn new(tag: &str) -> Dir {
let dir = std::env::temp_dir().join(format!("meebis-sock-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("could not make a temp dir");
Dir(dir)
}
fn sock(&self) -> PathBuf {
self.0.join("redis.sock")
}
}
impl Drop for Dir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
struct Server(Child);
impl Drop for Server {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn start(args: &[&str]) -> (Server, String) {
let mut child = Command::new(env!("CARGO_BIN_EXE_meebis"))
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to run the meebis binary");
let mut banner = String::new();
BufReader::new(child.stdout.take().expect("piped stdout"))
.read_line(&mut banner)
.expect("the server should announce itself");
(Server(child), banner)
}
fn ask(path: &Path, command: &str) -> String {
let mut sock = UnixStream::connect(path).expect("the socket should be accepting connections");
sock.write_all(format!("{command}\r\n").as_bytes())
.expect("write");
sock.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.expect("set a read timeout");
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
match sock.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
buf.extend_from_slice(&chunk[..n]);
if buf.ends_with(b"\r\n") {
break;
}
}
Err(e) => panic!("read failed: {e}"),
}
}
String::from_utf8_lossy(&buf).into_owned()
}
#[test]
fn a_socket_speaks_resp() {
let dir = Dir::new("resp");
let sock = dir.sock();
let (_server, banner) = start(&["--unixsocket", sock.to_str().unwrap()]);
assert!(
banner.contains(sock.to_str().unwrap()),
"the banner should name the socket, got {banner:?}"
);
assert_eq!(ask(&sock, "PING"), "+PONG\r\n");
assert_eq!(ask(&sock, "SET hello world"), "+OK\r\n");
assert_eq!(ask(&sock, "GET hello"), "$5\r\nworld\r\n");
}
#[test]
fn the_socket_replaces_the_port() {
let dir = Dir::new("noport");
let sock = dir.sock();
let (_server, banner) = start(&["--unixsocket", sock.to_str().unwrap()]);
assert!(
!banner.contains("127.0.0.1:"),
"a lone --unixsocket should not bind TCP, got {banner:?}"
);
assert_eq!(ask(&sock, "PING"), "+PONG\r\n");
}
#[test]
fn an_explicit_port_listens_on_both() {
let dir = Dir::new("both");
let sock = dir.sock();
let port_file = dir.0.join("port");
let (_server, banner) = start(&[
"--unixsocket",
sock.to_str().unwrap(),
"--port",
"0",
"--port-file",
port_file.to_str().unwrap(),
]);
assert!(
banner.contains(" and "),
"the banner should name both addresses, got {banner:?}"
);
assert_eq!(ask(&sock, "SET shared yes"), "+OK\r\n");
let port: u16 = std::fs::read_to_string(&port_file)
.expect("the port file should exist")
.trim()
.parse()
.expect("a numeric port");
let mut tcp = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect over TCP");
tcp.write_all(b"GET shared\r\n").expect("write");
let mut buf = [0u8; 9];
tcp.read_exact(&mut buf).expect("read");
assert_eq!(&buf, b"$3\r\nyes\r\n");
}
#[test]
fn client_list_reports_the_socket_path() {
let dir = Dir::new("clientlist");
let sock = dir.sock();
let (_server, _) = start(&["--unixsocket", sock.to_str().unwrap()]);
let info = ask(&sock, "CLIENT INFO");
let expected = format!("addr={}:0", sock.display());
assert!(
info.contains(&expected),
"expected {expected:?} in {info:?}"
);
assert!(
info.contains(&format!("laddr={}:0", sock.display())),
"laddr should be the socket, got {info:?}"
);
}
#[test]
fn a_clean_exit_removes_the_socket() {
let dir = Dir::new("cleanup");
let sock = dir.sock();
let (server, _) = start(&["--unixsocket", sock.to_str().unwrap()]);
assert!(sock.exists(), "the socket should exist while running");
let mut conn = UnixStream::connect(&sock).expect("connect");
conn.write_all(b"SHUTDOWN\r\n").expect("write");
let mut server = server;
server.0.wait().expect("the server should exit");
assert!(
!sock.exists(),
"a clean exit should unlink the socket it created"
);
}
#[test]
fn a_stale_socket_does_not_block_the_next_boot() {
let dir = Dir::new("stale");
let sock = dir.sock();
{
let (_server, _) = start(&["--unixsocket", sock.to_str().unwrap()]);
}
assert!(sock.exists(), "the killed server should leave the file");
let (_server, _) = start(&["--unixsocket", sock.to_str().unwrap()]);
assert_eq!(ask(&sock, "PING"), "+PONG\r\n");
}
#[test]
fn a_second_server_will_not_steal_a_live_socket() {
let dir = Dir::new("contested");
let sock = dir.sock();
let (_first, _) = start(&["--unixsocket", sock.to_str().unwrap()]);
let out = Command::new(env!("CARGO_BIN_EXE_meebis"))
.args(["--unixsocket", sock.to_str().unwrap()])
.output()
.expect("failed to run the meebis binary");
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("already listening"),
"unhelpful message: {stderr:?}"
);
assert_eq!(ask(&sock, "PING"), "+PONG\r\n");
}
#[test]
fn run_hands_the_socket_to_the_command() {
let dir = Dir::new("run");
let sock = dir.sock();
let out = Command::new(env!("CARGO_BIN_EXE_meebis"))
.args([
"run",
"--unixsocket",
sock.to_str().unwrap(),
"--",
"sh",
"-c",
"echo \"$REDIS_URL|$REDIS_SOCKET|${REDIS_HOST-unset}|${REDIS_PORT-unset}\"",
])
.env("REDIS_HOST", "stale.example")
.env("REDIS_PORT", "1234")
.output()
.expect("failed to run the meebis binary");
assert_eq!(out.status.code(), Some(0));
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
let parts: Vec<&str> = line.split('|').collect();
assert_eq!(parts.len(), 4, "unexpected output {line:?}");
let canonical = std::fs::canonicalize(&dir.0)
.expect("canonicalize")
.join("redis.sock");
assert_eq!(parts[0], format!("unix://{}", canonical.display()));
assert_eq!(parts[1], canonical.to_str().unwrap());
assert_eq!(parts[2], "unset", "a stale REDIS_HOST should be removed");
assert_eq!(parts[3], "unset", "a stale REDIS_PORT should be removed");
}
#[test]
fn port_file_without_a_port_is_a_usage_error() {
let dir = Dir::new("portfile");
let out = Command::new(env!("CARGO_BIN_EXE_meebis"))
.args([
"--unixsocket",
dir.sock().to_str().unwrap(),
"--port-file",
dir.0.join("port").to_str().unwrap(),
])
.output()
.expect("failed to run the meebis binary");
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--port-file"), "got {stderr:?}");
}