#![cfg(feature = "wire")]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::Duration;
use plushie::event::SystemEventType;
use plushie::prelude::*;
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()
}
fn write_wrapper(renderer: &str) -> std::path::PathBuf {
let mut wrapper = std::env::temp_dir();
wrapper.push(format!(
"plushie-wire-image-test-{}-{}.sh",
std::process::id(),
rand_suffix()
));
let script = format!("#!/bin/sh\nexec {renderer} --mock --json \"$@\"\n");
std::fs::write(&wrapper, script).expect("write wrapper script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&wrapper).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&wrapper, perms).unwrap();
}
wrapper
}
struct WrapperCleanup(std::path::PathBuf);
impl Drop for WrapperCleanup {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn rand_suffix() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
struct Shared {
init_ran: Arc<AtomicBool>,
handles: Arc<std::sync::Mutex<Option<Vec<String>>>>,
init_tx: Arc<std::sync::Mutex<Option<mpsc::Sender<()>>>>,
done_tx: Arc<std::sync::Mutex<Option<mpsc::Sender<()>>>>,
}
static SHARED: std::sync::OnceLock<std::sync::Mutex<Option<Shared>>> = std::sync::OnceLock::new();
fn install_shared(shared: Shared) {
let slot = SHARED.get_or_init(|| std::sync::Mutex::new(None));
*slot.lock().unwrap() = Some(shared);
}
fn take_shared() -> Shared {
SHARED
.get()
.expect("shared must be installed before run_wire")
.lock()
.unwrap()
.take()
.expect("shared already taken")
}
struct ImageApp {
shared: Shared,
}
impl App for ImageApp {
type Model = Self;
fn init() -> (Self, Command) {
let shared = take_shared();
shared.init_ran.store(true, Ordering::SeqCst);
if let Some(tx) = shared.init_tx.lock().unwrap().take() {
let _ = tx.send(());
}
let png = vec![
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x78,
0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0xF0, 0x1F, 0x00, 0x05, 0x00, 0x01, 0xFF, 0x89, 0x99,
0x3D, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
let rgba = vec![0u8; 2 * 2 * 4];
let rgba_updated = vec![0xffu8; 2 * 2 * 4];
let cmd = Command::batch([
Command::create_image("logo", png),
Command::create_image_rgba("pixels", 2, 2, rgba),
Command::update_image_rgba("pixels", 2, 2, rgba_updated),
Command::delete_image("logo"),
Command::list_images("check"),
]);
let model = Self { shared };
(model, cmd)
}
fn update(model: &mut Self, event: Event) -> Command {
if let Event::System(sys) = &event
&& sys.event_type == SystemEventType::ImageList
&& sys.tag.as_deref() == Some("check")
{
let handles = sys
.value
.as_ref()
.and_then(|v| v.get("handles"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect::<Vec<_>>()
})
.unwrap_or_default();
*model.shared.handles.lock().unwrap() = Some(handles);
if let Some(tx) = model.shared.done_tx.lock().unwrap().take() {
let _ = tx.send(());
}
}
Command::none()
}
fn view(_model: &Self, _widgets: &mut WidgetRegistrar) -> ViewList {
window("main").child(text("image ops").id("display")).into()
}
fn restart_policy() -> plushie::settings::RestartPolicy {
let mut policy = plushie::settings::RestartPolicy::default();
policy.max_restarts = 0;
policy.restart_delay = Duration::from_millis(10);
policy.heartbeat_interval = Some(Duration::from_secs(5));
policy
}
}
#[test]
fn wire_image_ops_survive_round_trip() {
let binary = plushie_binary();
if !std::path::Path::new(&binary).exists() {
eprintln!(
"wire_image_ops_survive_round_trip: renderer binary not found at {binary}; \
build it with `cargo build -p plushie-renderer` before running this test."
);
return;
}
let init_ran = Arc::new(AtomicBool::new(false));
let handles = Arc::new(std::sync::Mutex::new(None));
let (init_tx, init_rx) = mpsc::channel::<()>();
let (done_tx, done_rx) = mpsc::channel::<()>();
install_shared(Shared {
init_ran: init_ran.clone(),
handles: handles.clone(),
init_tx: Arc::new(std::sync::Mutex::new(Some(init_tx))),
done_tx: Arc::new(std::sync::Mutex::new(Some(done_tx))),
});
let wrapper = write_wrapper(&binary);
let wrapper_path = wrapper.to_string_lossy().into_owned();
let _cleanup = WrapperCleanup(wrapper.clone());
let handle = std::thread::spawn(move || plushie::run_with_renderer::<ImageApp>(&wrapper_path));
init_rx
.recv_timeout(Duration::from_secs(10))
.expect("ImageApp::init must run within 10s");
assert!(init_ran.load(Ordering::SeqCst));
done_rx
.recv_timeout(Duration::from_secs(10))
.expect("list_images response must arrive within 10s; wire op-name mismatch?");
drop(_cleanup);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while std::time::Instant::now() < deadline && !handle.is_finished() {
std::thread::sleep(Duration::from_millis(50));
}
let _ = handle.join();
let observed = handles
.lock()
.unwrap()
.take()
.expect("handles slot must be populated by the list_images response");
assert_eq!(
observed,
vec!["pixels".to_string()],
"image registry handles after the round trip",
);
}