use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
const RECV_TIMEOUT: Duration = Duration::from_secs(10);
fn plushie_binary() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("plushie-renderer");
path.to_string_lossy().to_string()
}
struct LineReceiver {
rx: mpsc::Receiver<serde_json::Value>,
_handle: std::thread::JoinHandle<()>,
}
impl LineReceiver {
fn new(stdout: ChildStdout) -> Self {
let (tx, rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let val: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(e) => panic!("JSON parse error: {e}\nraw: {line:?}"),
};
if tx.send(val).is_err() {
break;
}
}
Err(_) => break,
}
}
});
Self {
rx,
_handle: handle,
}
}
fn recv(&self) -> serde_json::Value {
self.rx
.recv_timeout(RECV_TIMEOUT)
.expect("subprocess did not respond within 10s")
}
}
fn send(stdin: &mut ChildStdin, msg: &serde_json::Value) {
let line = serde_json::to_string(msg).unwrap();
writeln!(stdin, "{line}").unwrap();
stdin.flush().unwrap();
}
fn spawn_renderer() -> Option<(Child, ChildStdin, LineReceiver)> {
let binary = plushie_binary();
if !std::path::Path::new(&binary).exists() {
eprintln!(
"skipping window_ops_subprocess: renderer binary not found at {binary}; \
build it with `cargo build -p plushie-renderer`"
);
return None;
}
let mut child = Command::new(&binary)
.args(["--mock", "--json"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn plushie-renderer");
let stdin = child.stdin.take().unwrap();
let stdout = LineReceiver::new(child.stdout.take().unwrap());
Some((child, stdin, stdout))
}
fn drain_until(stdout: &LineReceiver, mut pred: impl FnMut(&serde_json::Value) -> bool) {
loop {
let msg = stdout.recv();
if msg.get("type").and_then(|v| v.as_str()) == Some("event")
&& msg.get("family").and_then(|v| v.as_str()) == Some("session_error")
{
panic!("subprocess emitted a session_error during WindowOp dispatch: {msg}");
}
if pred(&msg) {
return;
}
}
}
#[test]
fn window_ops_dispatch_through_subprocess_without_error() {
let Some((mut child, mut stdin, stdout)) = spawn_renderer() else {
return;
};
send(
&mut stdin,
&serde_json::json!({
"session": "s1",
"type": "settings",
"settings": {"protocol_version": 1},
}),
);
let hello = stdout.recv();
assert_eq!(hello["type"], "hello");
send(
&mut stdin,
&serde_json::json!({
"session": "s1",
"type": "snapshot",
"tree": {
"id": "main",
"type": "window",
"props": {},
"children": [],
},
}),
);
for op in [
serde_json::json!({
"session": "s1",
"type": "window_op",
"op": "resize",
"window_id": "main",
"payload": {"width": 800.0, "height": 600.0},
}),
serde_json::json!({
"session": "s1",
"type": "window_op",
"op": "move",
"window_id": "main",
"payload": {"x": 50.0, "y": 75.0},
}),
serde_json::json!({
"session": "s1",
"type": "window_op",
"op": "close",
"window_id": "main",
"payload": {},
}),
] {
send(&mut stdin, &op);
}
send(
&mut stdin,
&serde_json::json!({
"session": "s1",
"type": "reset",
"id": "r1",
}),
);
drain_until(&stdout, |msg| {
msg["type"] == "reset_response" && msg["id"] == "r1"
});
drop(stdin);
let _ = child.wait();
}
#[test]
fn partial_window_close_in_one_session_does_not_disturb_another() {
let binary = plushie_binary();
if !std::path::Path::new(&binary).exists() {
eprintln!("skipping partial_window_close test: renderer binary not found");
return;
}
let mut child = Command::new(&binary)
.args(["--mock", "--json", "--max-sessions", "4"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn plushie-renderer");
let mut stdin = child.stdin.take().unwrap();
let stdout = LineReceiver::new(child.stdout.take().unwrap());
send(
&mut stdin,
&serde_json::json!({
"session": "s1",
"type": "settings",
"settings": {"protocol_version": 1},
}),
);
let hello = stdout.recv();
assert_eq!(hello["type"], "hello");
for sid in ["s1", "s2"] {
send(
&mut stdin,
&serde_json::json!({
"session": sid,
"type": "snapshot",
"tree": {
"id": format!("{sid}-window"),
"type": "window",
"props": {},
"children": [],
},
}),
);
}
send(
&mut stdin,
&serde_json::json!({
"session": "s1",
"type": "window_op",
"op": "close",
"window_id": "s1-window",
"payload": {},
}),
);
send(
&mut stdin,
&serde_json::json!({
"session": "s2",
"type": "query",
"id": "q1",
"target": "tree",
"selector": {},
}),
);
drain_until(&stdout, |msg| {
msg["type"] == "query_response" && msg["session"] == "s2" && msg["id"] == "q1"
});
drop(stdin);
let _ = child.wait();
}