ledger_sim/
handle.rs

1//! Speculos runtime handle, provides out-of-band interaction with a simulator instance
2//! via the [HTTP API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/LedgerHQ/speculos/master/speculos/api/static/swagger/swagger.json) to allow button pushes and screenshots when executing integration tests.
3//!
4//!
5
6use std::{io::Cursor, net::SocketAddr};
7
8use async_trait::async_trait;
9use image::{io::Reader as ImageReader, DynamicImage};
10use reqwest::Client;
11use serde::{Deserialize, Serialize};
12use strum::Display;
13use tracing::debug;
14
15use crate::GenericHandle;
16
17/// Button enumeration
18#[derive(Clone, Copy, PartialEq, Debug, Display)]
19#[strum(serialize_all = "kebab-case")]
20pub enum Button {
21    Left,
22    Right,
23    Both,
24}
25
26/// Button actions
27#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display)]
28#[serde(rename_all = "kebab-case")]
29pub enum Action {
30    Press,
31    Release,
32    PressAndRelease,
33}
34
35/// Button action object for serialisation and use with the HTTP API
36#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
37struct ButtonAction {
38    pub action: Action,
39}
40
41/// [Handle] trait for interacting with speculos
42#[async_trait]
43pub trait Handle {
44    /// Get speculos HTTP address
45    fn addr(&self) -> SocketAddr;
46
47    /// Send a button action to the simulator
48    async fn button(&self, button: Button, action: Action) -> anyhow::Result<()> {
49        debug!("Sending button request: {}:{}", button, action);
50
51        // Post action to HTTP API
52        let r = Client::new()
53            .post(format!("http://{}/button/{}", self.addr(), button))
54            .json(&ButtonAction { action })
55            .send()
56            .await?;
57
58        debug!("Button request complete: {}", r.status());
59
60        Ok(())
61    }
62
63    /// Fetch a screenshot from the simulator
64    async fn screenshot(&self) -> anyhow::Result<DynamicImage> {
65        // Fetch screenshot from HTTP API
66        let r = reqwest::get(format!("http://{}/screenshot", self.addr())).await?;
67
68        // Read image bytes
69        let b = r.bytes().await?;
70
71        // Parse image object
72        let i = ImageReader::new(Cursor::new(b))
73            .with_guessed_format()?
74            .decode()?;
75
76        Ok(i)
77    }
78}
79
80impl Handle for GenericHandle {
81    fn addr(&self) -> SocketAddr {
82        match self {
83            GenericHandle::Local(h) => h.addr(),
84            GenericHandle::Docker(h) => h.addr(),
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    /// Check button string encoding
94    #[test]
95    fn button_encoding() {
96        let tests = &[
97            (Button::Left, "left"),
98            (Button::Right, "right"),
99            (Button::Both, "both"),
100        ];
101
102        for (v, s) in tests {
103            assert_eq!(&v.to_string(), s);
104        }
105    }
106
107    /// Check button action encoding
108    #[test]
109    fn action_encoding() {
110        let tests = &[
111            (
112                ButtonAction {
113                    action: Action::Press,
114                },
115                r#"{"action":"press"}"#,
116            ),
117            (
118                ButtonAction {
119                    action: Action::Release,
120                },
121                r#"{"action":"release"}"#,
122            ),
123            (
124                ButtonAction {
125                    action: Action::PressAndRelease,
126                },
127                r#"{"action":"press-and-release"}"#,
128            ),
129        ];
130
131        for (v, s) in tests {
132            assert_eq!(&serde_json::to_string(v).unwrap(), s);
133        }
134    }
135}