waos-dev-protocol 0.2.0

What cargo waos and a running WaOS say to each other: the messages of one run, their frames, and the Unix socket that carries them.
Documentation
//! The messages over a real socket pair, after the two sides agreed on the protocol, a client of
//! another build refused, and what the listener does with the socket file: a stale one replaced,
//! a second WaOS refused, and the connection closing seen as none.

#![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,
};

/// A folder of this test's own, with its socket's path inside; removed with the folder.
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);
    }
}

/// A listener at `path` and a connection to it, with the listener's end of that connection,
/// the two sides agreed on the protocol.
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)
}

/// A client built from another waos-dev-protocol, one that speaks protocol `PROTOCOL + 1`: it sends its
/// number and gets WaOS's back, the one frame every build reads, and WaOS refuses it naming
/// both numbers and closes.
#[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();
    // Bound and dropped, as a WaOS that died leaves it: the file stays, nobody answers.
    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
    );
}