polyhorn_cli/test/
mod.rs

1//! Types to run a test feedback server.
2
3use futures::channel::oneshot;
4use futures::FutureExt;
5use serde::Deserialize;
6use std::net::{IpAddr, Ipv4Addr, SocketAddr};
7use std::sync::mpsc;
8use std::sync::{Arc, Mutex};
9use warp::Filter;
10
11mod device;
12mod error;
13mod output;
14mod snapshot;
15
16pub use device::Device;
17pub use error::Error;
18pub use output::Output;
19pub use snapshot::Metadata;
20
21/// Disposable that can be dropped to stop a test feedback server.
22pub struct Disposable(Option<oneshot::Sender<mpsc::Sender<()>>>);
23
24/// Message that is sent to the test feedback server.
25#[derive(Debug, Deserialize)]
26pub enum Message {
27    /// Creates a screenshot of the device and assigns it to the given test and
28    /// snapshot names.
29    Snapshot {
30        /// Test name to attach to the screenshot.
31        test_name: String,
32
33        /// Name to attach to the snapshot. This field can be used to
34        /// distinguish between multiple snapshots within the same test.
35        snapshot_name: String,
36    },
37
38    /// Opens the given URL on the device.
39    OpenURL(String),
40}
41
42impl Drop for Disposable {
43    fn drop(&mut self) {
44        if let Some(sender) = self.0.take() {
45            let (tx, rx) = mpsc::channel();
46            let _ = sender.send(tx);
47            rx.recv().unwrap();
48        }
49    }
50}
51
52fn handler(device: Device) -> impl warp::Filter<Extract = impl warp::Reply> + Clone {
53    let output = Arc::new(Mutex::new(Output::new("target/polyhorn-snapshots")));
54
55    warp::path!("polyhorn" / "tests" / String)
56        .and(warp::post())
57        .and(warp::body::json())
58        .map(move |_id, message: Message| {
59            match message {
60                Message::OpenURL(url) => {
61                    device.open_url(&url).unwrap();
62                }
63                Message::Snapshot {
64                    test_name,
65                    snapshot_name,
66                } => {
67                    let screenshot = device.screenshot().unwrap();
68
69                    output.lock().unwrap().store(
70                        snapshot::Metadata {
71                            test_name: Some(test_name),
72                            snapshot_name: Some(snapshot_name),
73                            ..device.metadata()
74                        },
75                        screenshot,
76                    );
77                }
78            }
79
80            "Ok"
81        })
82}
83
84/// Starts a test feedback server for the given device. This function returns
85/// the address (incl. port) that the server is bound to, and a disposable that
86/// can be dropped to stop the server.
87pub fn serve(device: Device) -> (SocketAddr, Disposable) {
88    let (sender, receiver) = std::sync::mpsc::channel();
89
90    std::thread::spawn(move || {
91        let mut runtime = tokio::runtime::Runtime::new().unwrap();
92        runtime.block_on(async move {
93            let (drop_sender, drop_receiver) = oneshot::channel();
94
95            let server = warp::serve(handler(device));
96
97            let (addr, runloop) =
98                server.bind_ephemeral(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0));
99
100            sender.send((addr, Disposable(Some(drop_sender)))).unwrap();
101
102            let mut drop_receiver = drop_receiver.fuse();
103            let mut runloop = runloop.fuse();
104
105            futures::select! {
106                tx = drop_receiver => {
107                    std::mem::drop(runloop);
108
109                    tx.unwrap().send(()).unwrap();
110                },
111                _ = runloop => {}
112            };
113        });
114    });
115
116    receiver.recv().unwrap()
117}