#![cfg(unix)]
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use waos_dev_protocol::{
Connection, FromWaOS, Input, Listener, OutputStream, PROTOCOL, RunEnd, ToWaOS, Unreached,
WindowPicture,
};
struct TestFolder(PathBuf);
impl TestFolder {
fn new(test: &str) -> TestFolder {
let folder = std::env::temp_dir().join(format!("waos-dev-{}-{test}", std::process::id()));
let _ = fs::remove_dir_all(&folder);
TestFolder(folder)
}
fn socket(&self) -> PathBuf {
self.0.join("dev.sock")
}
}
impl Drop for TestFolder {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
async fn connected(path: &Path) -> (Listener, Connection<UnixStream>, Connection<UnixStream>) {
let listener = Listener::bind(path).await.unwrap();
let (client, server) = tokio::join!(Connection::connect(path), listener.accept());
let (mut client, mut server) = (client.unwrap(), server.unwrap());
let (agreed, agreed_too) = tokio::join!(client.agree_on_protocol(), server.agree_on_protocol());
agreed.unwrap();
agreed_too.unwrap();
(listener, client, server)
}
#[tokio::test]
async fn a_client_of_another_build_is_refused_with_both_numbers() {
let folder = TestFolder::new("other-build");
let socket = folder.socket();
let listener = Listener::bind(&socket).await.unwrap();
let (client, server) = tokio::join!(UnixStream::connect(&socket), listener.accept());
let (mut client, mut server) = (client.unwrap(), server.unwrap());
let theirs = PROTOCOL + 1;
let mut their_frame = 4u32.to_le_bytes().to_vec();
their_frame.extend_from_slice(&theirs.to_le_bytes());
client.write_all(&their_frame).await.unwrap();
let refused = server.agree_on_protocol().await.unwrap_err();
assert_eq!(refused.kind(), ErrorKind::Unsupported);
assert!(
refused.to_string().contains(&format!("protocol {theirs}"))
&& refused
.to_string()
.contains(&format!("protocol {PROTOCOL}")),
"{refused}"
);
drop(server);
let mut ours = [0; 8];
client.read_exact(&mut ours).await.unwrap();
assert_eq!(&ours[..4], 4u32.to_le_bytes());
assert_eq!(u32::from_le_bytes(ours[4..].try_into().unwrap()), PROTOCOL);
assert_eq!(client.read(&mut [0; 1]).await.unwrap(), 0, "WaOS closed");
}
fn every_from_waos() -> Vec<FromWaOS> {
vec![
FromWaOS::Started,
FromWaOS::Output {
stream: OutputStream::Stdout,
bytes: b"hello, ".to_vec(),
},
FromWaOS::Output {
stream: OutputStream::Stderr,
bytes: b"a warning\n".to_vec(),
},
FromWaOS::Ended(RunEnd::Returned { success: true }),
FromWaOS::Ended(RunEnd::Returned { success: false }),
FromWaOS::Ended(RunEnd::Failed("unreachable".into())),
FromWaOS::Ended(RunEnd::Stopped),
FromWaOS::Ended(RunEnd::Replaced),
FromWaOS::Refused("not a component".into()),
FromWaOS::Opened,
FromWaOS::Screenshots(vec![
WindowPicture {
window: 1,
title: "Notes".into(),
width: 1600,
height: 1200,
in_sight: true,
png: Some((0..=255).cycle().take(1 << 20).collect()),
},
WindowPicture {
window: 2,
title: String::new(),
width: 800,
height: 600,
in_sight: false,
png: None,
},
]),
FromWaOS::InputDone { new_picture: true },
FromWaOS::InputDone { new_picture: false },
FromWaOS::Unreached(Unreached::NotRunByCargoWaos),
FromWaOS::Unreached(Unreached::NoWindow {
window: 3,
windows: 1,
}),
FromWaOS::Unreached(Unreached::OutsideWindow {
width: 1600,
height: 1200,
}),
FromWaOS::Unreached(Unreached::NotEditing),
FromWaOS::Unreached(Unreached::UnknownKey("hyper+q".into())),
]
}
fn every_to_waos() -> Vec<ToWaOS> {
let id: waos_package::ProgramId = "dev.example.triangle".parse().unwrap();
vec![
ToWaOS::Run {
id: id.clone(),
component: (0..=255).cycle().take(3 << 20).collect(),
},
ToWaOS::Open {
id: id.clone(),
files: vec!["/Users/me/notes.md".into()],
},
ToWaOS::Screenshot { id: id.clone() },
ToWaOS::Input {
id: id.clone(),
window: 1,
input: Input::Tap { x: 1.5, y: 2.5 },
},
ToWaOS::Input {
id: id.clone(),
window: 1,
input: Input::Text("héllo".into()),
},
ToWaOS::Input {
id: id.clone(),
window: 2,
input: Input::Key("cmd+shift+z".into()),
},
ToWaOS::Input {
id,
window: 1,
input: Input::Scroll {
x: 3.0,
y: 4.0,
dx: -5.0,
dy: 6.0,
},
},
]
}
#[tokio::test]
async fn every_message_comes_back_as_sent() {
let folder = TestFolder::new("round-trip");
let (_listener, mut client, mut server) = connected(&folder.socket()).await;
for message in every_to_waos() {
let (sent, received) = tokio::join!(client.send(&message), server.receive::<ToWaOS>());
sent.unwrap();
assert_eq!(received.unwrap(), Some(message));
}
for message in every_from_waos() {
let (sent, received) = tokio::join!(server.send(&message), client.receive::<FromWaOS>());
sent.unwrap();
assert_eq!(received.unwrap(), Some(message));
}
}
#[tokio::test]
async fn the_halves_send_while_they_watch_for_the_close() {
let folder = TestFolder::new("halves");
let (_listener, mut client, server) = connected(&folder.socket()).await;
let (mut server_in, mut server_out) = server.into_split();
let watching = tokio::spawn(async move { server_in.receive::<ToWaOS>().await });
server_out.send(&FromWaOS::Started).await.unwrap();
assert_eq!(
client.receive::<FromWaOS>().await.unwrap(),
Some(FromWaOS::Started)
);
drop(client);
assert_eq!(watching.await.unwrap().unwrap(), None, "the close is seen");
}
#[tokio::test]
async fn the_other_side_closing_is_none() {
let folder = TestFolder::new("closing");
let (_listener, client, mut server) = connected(&folder.socket()).await;
drop(client);
assert_eq!(server.receive::<ToWaOS>().await.unwrap(), None);
}
#[tokio::test]
async fn a_socket_file_a_dead_waos_left_is_replaced() {
let folder = TestFolder::new("stale");
let socket = folder.socket();
fs::create_dir_all(&folder.0).unwrap();
drop(std::os::unix::net::UnixListener::bind(&socket).unwrap());
assert!(socket.exists());
let (_listener, mut client, mut server) = connected(&socket).await;
let (sent, received) = tokio::join!(server.send(&FromWaOS::Started), client.receive());
sent.unwrap();
assert_eq!(received.unwrap(), Some(FromWaOS::Started));
}
#[tokio::test]
async fn a_second_waos_is_refused_while_the_first_answers() {
let folder = TestFolder::new("second");
let socket = folder.socket();
let first = Listener::bind(&socket).await.unwrap();
let (refused, probe) = tokio::join!(Listener::bind(&socket), first.accept());
assert_eq!(refused.unwrap_err().kind(), ErrorKind::AddrInUse);
let mut probe = probe.unwrap();
assert_eq!(
probe.receive::<ToWaOS>().await.unwrap(),
None,
"the probe is a connection that asked nothing"
);
}
#[tokio::test]
async fn a_listener_dropped_leaves_no_socket_file() {
let folder = TestFolder::new("dropped");
let socket = folder.socket();
drop(Listener::bind(&socket).await.unwrap());
assert!(!socket.exists());
assert_eq!(
Connection::connect(&socket).await.unwrap_err().kind(),
ErrorKind::NotFound
);
}