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::ImePreedit {
195            text,
196            cursor_start,
197            cursor_end,
198        } => {
199            let cursor = match (cursor_start, cursor_end) {
200                (Some(start), Some(end)) => Some((start, end)),
201                _ => None,
202            };
203            inject_event(injector, TestEvent::ImePreedit { text, cursor });
204            TestResponse::Ok {}
205        }
206        TestCommand::ImeCommit { text } => {
207            inject_event(injector, TestEvent::ImeCommit { text });
208            TestResponse::Ok {}
209        }
210        TestCommand::ImeCancel {} => {
211            inject_event(injector, TestEvent::ImeCancel);
212            TestResponse::Ok {}
213        }
214        TestCommand::PressKey { key, modifiers } => {
215            inject_event(
216                injector,
217                TestEvent::KeyDown {
218                    key_code: key.clone(),
219                    modifiers,
220                },
221            );
222            inject_event(
223                injector,
224                TestEvent::KeyUp {
225                    key_code: key,
226                    modifiers,
227                },
228            );
229            TestResponse::Ok {}
230        }
231        TestCommand::Screenshot { path } => query_event(injector, |response_tx| {
232            TestEvent::Screenshot { path, response_tx }
233        }),
234        TestCommand::CaptureScreenshot {} => query_event(injector, |response_tx| {
235            TestEvent::CaptureScreenshot { response_tx }
236        }),
237        TestCommand::GetText {} => {
238            query_event(injector, |response_tx| TestEvent::GetText { response_tx })
239        }
240        TestCommand::GetTree {} => {
241            query_event(injector, |response_tx| TestEvent::GetTree { response_tx })
242        }
243        TestCommand::Wait { ms } => {
244            std::thread::sleep(std::time::Duration::from_millis(ms));
245            TestResponse::Ok {}
246        }
247        TestCommand::Pump {} => {
248            query_event(injector, |response_tx| TestEvent::Pump { response_tx })
249        }
250        TestCommand::Quit {} => {
251            inject_event(injector, TestEvent::Quit);
252            TestResponse::Ok {}
253        }
254        TestCommand::SimulateMouseMove { x, y } => {
255            inject_event(injector, TestEvent::MouseMove { x, y });
256            TestResponse::Ok {}
257        }
258        TestCommand::SimulateRightClick { x, y } => {
259            inject_event(injector, TestEvent::MouseMove { x, y });
260            inject_event(injector, TestEvent::MouseDown { x, y, button: 1 });
261            inject_event(injector, TestEvent::MouseUp { x, y, button: 1 });
262            TestResponse::Ok {}
263        }
264        TestCommand::SimulateResize { width, height } => {
265            inject_event(injector, TestEvent::Resize { width, height });
266            TestResponse::Ok {}
267        }
268    }
269}
270
271#[cfg(not(target_arch = "wasm32"))]
272fn query_event<F>(injector: &EventInjector, make_event: F) -> TestResponse
273where
274    F: FnOnce(ResponseSender) -> TestEvent,
275{
276    let (response_tx, response_rx) = mpsc::channel();
277    inject_event(injector, make_event(response_tx));
278    wait_for_response(&response_rx)
279}
280
281#[cfg(not(target_arch = "wasm32"))]
282fn inject_event(injector: &EventInjector, event: TestEvent) {
283    match injector {
284        EventInjector::Proxy(proxy) => {
285            let _ = proxy.send_event(event);
286        }
287        EventInjector::Queue { queue, wake_proxy } => {
288            #[cfg(target_os = "android")]
289            let debug_android_events = std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
290            #[cfg(target_os = "android")]
291            if debug_android_events {
292                eprintln!("[android-debug] queue_inject={event:?}");
293            }
294            if let Ok(mut pending) = queue.lock() {
295                pending.push_back(event);
296                #[cfg(target_os = "android")]
297                if debug_android_events {
298                    eprintln!("[android-debug] queue_len={}", pending.len());
299                }
300            }
301            if let Some(proxy) = wake_proxy {
302                #[cfg(target_os = "android")]
303                if debug_android_events {
304                    eprintln!("[android-debug] wake_send");
305                }
306                let _ = proxy.send_event(TestEvent::Wake);
307            }
308        }
309    }
310}
311
312/// Block until the main event loop sends a response, with a 30-second timeout.
313#[cfg(not(target_arch = "wasm32"))]
314fn wait_for_response(rx: &ResponseReceiver) -> TestResponse {
315    match rx.recv_timeout(std::time::Duration::from_secs(30)) {
316        Ok(resp) => resp,
317        Err(_) => TestResponse::Error {
318            message: "timeout waiting for response from event loop".into(),
319        },
320    }
321}
322
323#[cfg(not(target_arch = "wasm32"))]
324fn send_http_response(stream: &mut TcpStream, status: u16, body: &str) {
325    let status_text = match status {
326        200 => "OK",
327        400 => "Bad Request",
328        404 => "Not Found",
329        500 => "Internal Server Error",
330        504 => "Gateway Timeout",
331        _ => "Unknown",
332    };
333    let response = format!(
334        "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
335        status, status_text, body.len(), body
336    );
337    let _ = stream.write_all(response.as_bytes());
338}