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