use std::time::Duration;
use crate::App;
use crate::automation::file::{Instruction, PlushieFile};
use crate::automation::runner::{self, RunResult};
use crate::runner::bridge::{Bridge, Codec, Incoming};
use crate::runner::wire_discovery;
use crate::test::TestSession;
use crate::{Error, Result as PlushieResult};
const DRAIN_POLL: Duration = Duration::from_millis(5);
const FINAL_FLUSH_PAUSE: Duration = Duration::from_millis(100);
pub fn run_windowed<A: App>(file: &PlushieFile) -> PlushieResult {
let binary = wire_discovery::discover_renderer()?;
run_windowed_with_renderer::<A>(&binary, file)
}
pub fn run_windowed_with_renderer<A: App>(binary: &str, file: &PlushieFile) -> PlushieResult {
log::info!("automation windowed: using renderer at {binary}");
let mut bridge = Bridge::spawn(binary).map_err(|e| Error::spawn(binary.to_string(), e))?;
let settings = build_automation_settings::<A>();
bridge.send_settings(&settings)?;
let hello = bridge
.receive()
.map_err(|e| Error::WireDecode(format!("hello: {e}")))?;
verify_protocol_version(&hello)?;
if let Some(codec) = hello.get("codec").and_then(|v| v.as_str()) {
let codec = match codec {
"msgpack" => Codec::MsgPack,
_ => Codec::Json,
};
bridge.set_codec(codec);
}
bridge.start_reader()?;
let mut session = TestSession::<A>::start().allow_diagnostics();
send_current_tree(&mut bridge, &session)?;
let mut passed = 0usize;
let mut failures: Vec<(usize, String)> = Vec::new();
for (line_no, instruction) in &file.instructions {
if let Instruction::Wait(ms) = instruction {
std::thread::sleep(Duration::from_millis(*ms));
passed += 1;
continue;
}
match execute_once(&mut session, instruction) {
Ok(()) => {
passed += 1;
}
Err(msg) => {
failures.push((*line_no, msg));
}
}
if let Err(e) = send_current_tree(&mut bridge, &session) {
log::warn!("windowed: tree refresh failed: {e}");
}
}
std::thread::sleep(FINAL_FLUSH_PAUSE);
let result = RunResult { passed, failures };
if result.is_ok() {
Ok(())
} else {
Err(Error::Startup(format!(
"{} instruction(s) failed",
result.failures.len()
)))
}
}
fn execute_once<A: App>(
session: &mut TestSession<A>,
instruction: &Instruction,
) -> Result<(), String> {
let single = PlushieFile {
header: crate::automation::file::Header::default(),
instructions: vec![(1, instruction.clone())],
};
let result = runner::run::<A>(&single, session);
if result.is_ok() {
Ok(())
} else {
Err(result
.failures
.into_iter()
.map(|(_, msg)| msg)
.next()
.unwrap_or_else(|| "unknown failure".to_string()))
}
}
fn verify_protocol_version(hello: &serde_json::Value) -> Result<(), Error> {
let expected = plushie_core::protocol::PROTOCOL_VERSION;
let got = hello_protocol_version(hello);
if got == Some(expected) {
Ok(())
} else {
Err(Error::ProtocolVersionMismatch { expected, got })
}
}
fn hello_protocol_version(hello: &serde_json::Value) -> Option<u32> {
hello
.get("protocol_version")
.or_else(|| hello.get("protocol"))
.and_then(plushie_core::protocol::json_protocol_version)
}
fn send_current_tree<A: App>(bridge: &mut Bridge, session: &TestSession<A>) -> PlushieResult {
let snapshot = serde_json::to_value(session.tree())
.map_err(|e| Error::WireEncode(format!("tree: {e}")))?;
bridge.send_snapshot(&snapshot)?;
while let Incoming::Message(_) = bridge.recv_timeout(Some(DRAIN_POLL)) {}
Ok(())
}
fn build_automation_settings<A: App>() -> serde_json::Value {
let mut json = A::settings().to_wire_json();
if let serde_json::Value::Object(ref mut map) = json {
map.insert(
"protocol_version".to_string(),
serde_json::json!(plushie_core::protocol::PROTOCOL_VERSION),
);
}
json
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_version_wins_over_legacy_protocol() {
let expected = plushie_core::protocol::PROTOCOL_VERSION;
let hello = serde_json::json!({
"protocol_version": expected,
"protocol": expected + 1,
});
assert_eq!(hello_protocol_version(&hello), Some(expected));
assert!(verify_protocol_version(&hello).is_ok());
}
#[test]
fn legacy_protocol_is_fallback() {
let expected = plushie_core::protocol::PROTOCOL_VERSION;
let hello = serde_json::json!({
"protocol": expected,
});
assert_eq!(hello_protocol_version(&hello), Some(expected));
assert!(verify_protocol_version(&hello).is_ok());
}
#[test]
fn out_of_range_protocol_is_rejected() {
let hello = serde_json::json!({
"protocol_version": u64::from(u32::MAX) + 1,
});
assert_eq!(hello_protocol_version(&hello), None);
assert!(verify_protocol_version(&hello).is_err());
}
#[test]
fn non_integer_protocol_is_rejected() {
let hello = serde_json::json!({
"protocol_version": 1.5,
});
assert_eq!(hello_protocol_version(&hello), None);
assert!(verify_protocol_version(&hello).is_err());
}
#[test]
fn u32_max_protocol_is_accepted() {
let hello = serde_json::json!({
"protocol_version": u32::MAX,
});
assert_eq!(hello_protocol_version(&hello), Some(u32::MAX));
}
}