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 = mpsc::Sender<TestResponse>;
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
36pub 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#[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#[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}