Skip to main content

fission_shell_winit/
test_control.rs

1#[cfg(not(target_arch = "wasm32"))]
2use fission_test_driver::TestCommand;
3#[cfg(not(target_arch = "wasm32"))]
4use fission_test_driver::TestEvent;
5use fission_test_driver::TestResponse;
6#[cfg(not(target_arch = "wasm32"))]
7use std::collections::VecDeque;
8#[cfg(not(target_arch = "wasm32"))]
9use std::io::{Read, Write};
10#[cfg(not(target_arch = "wasm32"))]
11use std::net::{TcpListener, TcpStream};
12use std::sync::mpsc;
13#[cfg(not(target_arch = "wasm32"))]
14use std::sync::{Arc, Mutex};
15#[cfg(not(target_arch = "wasm32"))]
16use winit::event_loop::EventLoopProxy;
17
18/// Sender for query responses from the main event loop back to the TCP server.
19pub type ResponseSender = fission_test_driver::TestResponseSender;
20/// Receiver for query responses.
21pub type ResponseReceiver = mpsc::Receiver<TestResponse>;
22/// Shared queue used on platforms where winit user events are unreliable.
23#[cfg(not(target_arch = "wasm32"))]
24pub type PendingEventQueue = Arc<Mutex<VecDeque<TestEvent>>>;
25
26#[cfg(not(target_arch = "wasm32"))]
27#[derive(Clone)]
28pub enum EventInjector {
29    Proxy(EventLoopProxy<TestEvent>),
30    Queue {
31        queue: PendingEventQueue,
32        wake_proxy: Option<EventLoopProxy<TestEvent>>,
33    },
34}
35
36#[cfg(not(target_arch = "wasm32"))]
37pub fn create_pending_event_queue() -> PendingEventQueue {
38    Arc::new(Mutex::new(VecDeque::new()))
39}
40
41/// Spawn the TCP test-control server.
42#[cfg(not(target_arch = "wasm32"))]
43pub fn spawn_server(port: u16, injector: EventInjector) -> std::thread::JoinHandle<()> {
44    std::thread::spawn(move || {
45        let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
46            .unwrap_or_else(|e| panic!("failed to bind test control port {}: {}", port, e));
47        eprintln!("[fission-test-control] listening on port {}", port);
48
49        for stream in listener.incoming() {
50            match stream {
51                Ok(stream) => handle_connection(stream, &injector),
52                Err(e) => eprintln!("[fission-test-control] accept error: {}", e),
53            }
54        }
55    })
56}
57
58#[cfg(not(target_arch = "wasm32"))]
59fn handle_connection(mut stream: TcpStream, injector: &EventInjector) {
60    let mut buf = Vec::new();
61    let mut tmp = [0u8; 4096];
62
63    loop {
64        match stream.read(&mut tmp) {
65            Ok(0) => return,
66            Ok(n) => {
67                buf.extend_from_slice(&tmp[..n]);
68                if buf.windows(4).any(|w| w == b"\r\n\r\n") {
69                    break;
70                }
71            }
72            Err(_) => return,
73        }
74    }
75
76    let request = String::from_utf8_lossy(&buf);
77    let first_line = request.lines().next().unwrap_or("");
78    let parts: Vec<&str> = first_line.split_whitespace().collect();
79    let method = parts.first().copied().unwrap_or("");
80    let path = parts.get(1).copied().unwrap_or("");
81
82    if path == "/health" {
83        send_http_response(&mut stream, 200, r#"{"status":"ok"}"#);
84        return;
85    }
86
87    if method != "POST" || path != "/cmd" {
88        send_http_response(
89            &mut stream,
90            404,
91            r#"{"status":"Error","message":"not found"}"#,
92        );
93        return;
94    }
95
96    let content_length = request
97        .lines()
98        .find(|line| line.to_lowercase().starts_with("content-length:"))
99        .and_then(|line| line.split(':').nth(1))
100        .and_then(|value| value.trim().parse::<usize>().ok())
101        .unwrap_or(0);
102
103    let header_end = buf
104        .windows(4)
105        .position(|w| w == b"\r\n\r\n")
106        .map(|pos| pos + 4)
107        .unwrap_or(buf.len());
108
109    let mut body = buf[header_end..].to_vec();
110    while body.len() < content_length {
111        match stream.read(&mut tmp) {
112            Ok(0) => break,
113            Ok(n) => body.extend_from_slice(&tmp[..n]),
114            Err(_) => break,
115        }
116    }
117
118    let body_str = String::from_utf8_lossy(&body);
119    let cmd: TestCommand = match serde_json::from_str(&body_str) {
120        Ok(cmd) => cmd,
121        Err(error) => {
122            let resp = TestResponse::Error {
123                message: format!("parse error: {}", error),
124            };
125            send_http_response(&mut stream, 400, &serde_json::to_string(&resp).unwrap());
126            return;
127        }
128    };
129
130    let response = dispatch_command(cmd, injector);
131    send_http_response(&mut stream, 200, &serde_json::to_string(&response).unwrap());
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135fn dispatch_command(cmd: TestCommand, injector: &EventInjector) -> TestResponse {
136    match cmd {
137        TestCommand::Tap { x, y } => {
138            inject_event(injector, TestEvent::MouseMove { x, y });
139            inject_event(injector, TestEvent::MouseDown { x, y, button: 0 });
140            inject_event(injector, TestEvent::MouseUp { x, y, button: 0 });
141            TestResponse::Ok {}
142        }
143        TestCommand::Drag {
144            start_x,
145            start_y,
146            end_x,
147            end_y,
148            steps,
149        } => {
150            let steps = steps.max(1);
151            inject_event(
152                injector,
153                TestEvent::MouseMove {
154                    x: start_x,
155                    y: start_y,
156                },
157            );
158            inject_event(
159                injector,
160                TestEvent::MouseDown {
161                    x: start_x,
162                    y: start_y,
163                    button: 0,
164                },
165            );
166            for step in 1..=steps {
167                let t = step as f32 / steps as f32;
168                let x = start_x + (end_x - start_x) * t;
169                let y = start_y + (end_y - start_y) * t;
170                inject_event(injector, TestEvent::MouseMove { x, y });
171            }
172            inject_event(
173                injector,
174                TestEvent::MouseUp {
175                    x: end_x,
176                    y: end_y,
177                    button: 0,
178                },
179            );
180            TestResponse::Ok {}
181        }
182        TestCommand::TapText { text } => query_event(injector, |response_tx| TestEvent::TapText {
183            text,
184            response_tx,
185        }),
186        TestCommand::Scroll { x, y, dx, dy } => {
187            inject_event(injector, TestEvent::Scroll { x, y, dx, dy });
188            TestResponse::Ok {}
189        }
190        TestCommand::TypeText { text } => {
191            inject_event(injector, TestEvent::TextInput { text });
192            TestResponse::Ok {}
193        }
194        TestCommand::PressKey { key, modifiers } => {
195            inject_event(
196                injector,
197                TestEvent::KeyDown {
198                    key_code: key.clone(),
199                    modifiers,
200                },
201            );
202            inject_event(
203                injector,
204                TestEvent::KeyUp {
205                    key_code: key,
206                    modifiers,
207                },
208            );
209            TestResponse::Ok {}
210        }
211        TestCommand::Screenshot { path } => query_event(injector, |response_tx| {
212            TestEvent::Screenshot { path, response_tx }
213        }),
214        TestCommand::CaptureScreenshot {} => query_event(injector, |response_tx| {
215            TestEvent::CaptureScreenshot { response_tx }
216        }),
217        TestCommand::GetText {} => {
218            query_event(injector, |response_tx| TestEvent::GetText { response_tx })
219        }
220        TestCommand::GetTree {} => {
221            query_event(injector, |response_tx| TestEvent::GetTree { response_tx })
222        }
223        TestCommand::Wait { ms } => {
224            std::thread::sleep(std::time::Duration::from_millis(ms));
225            TestResponse::Ok {}
226        }
227        TestCommand::Pump {} => {
228            query_event(injector, |response_tx| TestEvent::Pump { response_tx })
229        }
230        TestCommand::Quit {} => {
231            inject_event(injector, TestEvent::Quit);
232            TestResponse::Ok {}
233        }
234        TestCommand::SimulateMouseMove { x, y } => {
235            inject_event(injector, TestEvent::MouseMove { x, y });
236            TestResponse::Ok {}
237        }
238        TestCommand::SimulateRightClick { x, y } => {
239            inject_event(injector, TestEvent::MouseMove { x, y });
240            inject_event(injector, TestEvent::MouseDown { x, y, button: 1 });
241            inject_event(injector, TestEvent::MouseUp { x, y, button: 1 });
242            TestResponse::Ok {}
243        }
244        TestCommand::SimulateResize { width, height } => {
245            inject_event(injector, TestEvent::Resize { width, height });
246            TestResponse::Ok {}
247        }
248    }
249}
250
251#[cfg(not(target_arch = "wasm32"))]
252fn query_event<F>(injector: &EventInjector, make_event: F) -> TestResponse
253where
254    F: FnOnce(ResponseSender) -> TestEvent,
255{
256    let (response_tx, response_rx) = mpsc::channel();
257    inject_event(injector, make_event(response_tx));
258    wait_for_response(&response_rx)
259}
260
261#[cfg(not(target_arch = "wasm32"))]
262fn inject_event(injector: &EventInjector, event: TestEvent) {
263    match injector {
264        EventInjector::Proxy(proxy) => {
265            let _ = proxy.send_event(event);
266        }
267        EventInjector::Queue { queue, wake_proxy } => {
268            #[cfg(target_os = "android")]
269            let debug_android_events = std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
270            #[cfg(target_os = "android")]
271            if debug_android_events {
272                eprintln!("[android-debug] queue_inject={event:?}");
273            }
274            if let Ok(mut pending) = queue.lock() {
275                pending.push_back(event);
276                #[cfg(target_os = "android")]
277                if debug_android_events {
278                    eprintln!("[android-debug] queue_len={}", pending.len());
279                }
280            }
281            if let Some(proxy) = wake_proxy {
282                #[cfg(target_os = "android")]
283                if debug_android_events {
284                    eprintln!("[android-debug] wake_send");
285                }
286                let _ = proxy.send_event(TestEvent::Wake);
287            }
288        }
289    }
290}
291
292/// Block until the main event loop sends a response, with a 30-second timeout.
293#[cfg(not(target_arch = "wasm32"))]
294fn wait_for_response(rx: &ResponseReceiver) -> TestResponse {
295    match rx.recv_timeout(std::time::Duration::from_secs(30)) {
296        Ok(resp) => resp,
297        Err(_) => TestResponse::Error {
298            message: "timeout waiting for response from event loop".into(),
299        },
300    }
301}
302
303#[cfg(not(target_arch = "wasm32"))]
304fn send_http_response(stream: &mut TcpStream, status: u16, body: &str) {
305    let status_text = match status {
306        200 => "OK",
307        400 => "Bad Request",
308        404 => "Not Found",
309        500 => "Internal Server Error",
310        504 => "Gateway Timeout",
311        _ => "Unknown",
312    };
313    let response = format!(
314        "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
315        status, status_text, body.len(), body
316    );
317    let _ = stream.write_all(response.as_bytes());
318}