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
18pub type ResponseSender = fission_test_driver::TestResponseSender;
20pub type ResponseReceiver = mpsc::Receiver<TestResponse>;
22#[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#[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#[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}