@@ -3,18 +3,25 @@ mod redact;
33use std:: {
44 env:: { self , join_paths, split_paths} ,
55 ffi:: OsStr ,
6- io:: Write ,
76 path:: { Path , PathBuf } ,
8- process:: { Command , Stdio } ,
7+ process:: Stdio ,
98 sync:: Arc ,
9+ time:: Duration ,
1010} ;
1111
1212use copy_dir:: copy_dir;
1313use redact:: redact_e2e_output;
14+ use tokio:: {
15+ io:: { AsyncReadExt , AsyncWriteExt } ,
16+ process:: Command ,
17+ } ;
1418use vite_path:: { AbsolutePath , AbsolutePathBuf , RelativePathBuf } ;
1519use vite_str:: Str ;
1620use vite_workspace:: find_workspace_root;
1721
22+ /// Timeout for each step in e2e tests
23+ const STEP_TIMEOUT : Duration = Duration :: from_secs ( 10 ) ;
24+
1825/// Get the shell executable for running e2e test steps.
1926/// On Unix, uses /bin/sh.
2027/// On Windows, uses BASH env var or falls back to Git Bash.
@@ -77,7 +84,12 @@ struct SnapshotsFile {
7784 pub e2e_cases : Vec < E2e > ,
7885}
7986
80- fn run_case ( tmpdir : & AbsolutePath , fixture_path : & Path , filter : Option < & str > ) {
87+ fn run_case (
88+ runtime : & tokio:: runtime:: Runtime ,
89+ tmpdir : & AbsolutePath ,
90+ fixture_path : & Path ,
91+ filter : Option < & str > ,
92+ ) {
8193 let fixture_name = fixture_path. file_name ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) ;
8294 if fixture_name. starts_with ( "." ) {
8395 return ; // skip hidden files like .DS_Store
@@ -96,10 +108,11 @@ fn run_case(tmpdir: &AbsolutePath, fixture_path: &Path, filter: Option<&str>) {
96108 settings. set_prepend_module_to_snapshot ( false ) ;
97109 settings. remove_snapshot_suffix ( ) ;
98110
99- settings. bind ( || run_case_inner ( tmpdir, fixture_path, fixture_name) ) ;
111+ // Use block_on inside bind to run async code with insta settings applied
112+ settings. bind ( || runtime. block_on ( run_case_inner ( tmpdir, fixture_path, fixture_name) ) ) ;
100113}
101114
102- fn run_case_inner ( tmpdir : & AbsolutePath , fixture_path : & Path , fixture_name : & str ) {
115+ async fn run_case_inner ( tmpdir : & AbsolutePath , fixture_path : & Path , fixture_name : & str ) {
103116 // Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case.
104117 let stage_path = tmpdir. join ( fixture_name) ;
105118 copy_dir ( fixture_path, & stage_path) . unwrap ( ) ;
@@ -175,30 +188,98 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &Path, fixture_name: &str
175188 }
176189 }
177190
178- let output = if let Some ( stdin_content) = step. stdin ( ) {
179- cmd. stdin ( Stdio :: piped ( ) ) ;
180- cmd. stdout ( Stdio :: piped ( ) ) ;
181- cmd. stderr ( Stdio :: piped ( ) ) ;
182- let mut child = cmd. spawn ( ) . unwrap ( ) ;
183- child. stdin . take ( ) . unwrap ( ) . write_all ( stdin_content. as_bytes ( ) ) . unwrap ( ) ;
184- child. wait_with_output ( ) . unwrap ( )
185- } else {
186- cmd. output ( ) . unwrap ( )
187- } ;
191+ // Spawn the child process
192+ cmd. stdin ( if step. stdin ( ) . is_some ( ) { Stdio :: piped ( ) } else { Stdio :: null ( ) } ) ;
193+ cmd. stdout ( Stdio :: piped ( ) ) ;
194+ cmd. stderr ( Stdio :: piped ( ) ) ;
188195
189- let exit_code = output. status . code ( ) . unwrap_or ( -1 ) ;
190- if exit_code != 0 {
191- e2e_outputs. push_str ( format ! ( "[{}]" , exit_code) . as_str ( ) ) ;
196+ let mut child = cmd. spawn ( ) . unwrap ( ) ;
197+
198+ // Write stdin if provided, then close it
199+ if let Some ( stdin_content) = step. stdin ( ) {
200+ let mut stdin = child. stdin . take ( ) . unwrap ( ) ;
201+ stdin. write_all ( stdin_content. as_bytes ( ) ) . await . unwrap ( ) ;
202+ drop ( stdin) ; // Close stdin to signal EOF
192203 }
204+
205+ // Take stdout/stderr handles
206+ let mut stdout_handle = child. stdout . take ( ) . unwrap ( ) ;
207+ let mut stderr_handle = child. stderr . take ( ) . unwrap ( ) ;
208+
209+ // Buffers for accumulating output
210+ let mut stdout_buf = Vec :: new ( ) ;
211+ let mut stderr_buf = Vec :: new ( ) ;
212+
213+ // Read chunks concurrently with process wait, using select! with timeout
214+ let mut stdout_done = false ;
215+ let mut stderr_done = false ;
216+ let mut timed_out = false ;
217+ let mut exit_status: Option < std:: process:: ExitStatus > = None ;
218+
219+ let timeout = tokio:: time:: sleep ( STEP_TIMEOUT ) ;
220+ tokio:: pin!( timeout) ;
221+
222+ loop {
223+ let mut stdout_chunk = [ 0u8 ; 8192 ] ;
224+ let mut stderr_chunk = [ 0u8 ; 8192 ] ;
225+
226+ tokio:: select! {
227+ result = stdout_handle. read( & mut stdout_chunk) , if !stdout_done => {
228+ match result {
229+ Ok ( 0 ) => stdout_done = true ,
230+ Ok ( n) => stdout_buf. extend_from_slice( & stdout_chunk[ ..n] ) ,
231+ Err ( _) => stdout_done = true ,
232+ }
233+ }
234+ result = stderr_handle. read( & mut stderr_chunk) , if !stderr_done => {
235+ match result {
236+ Ok ( 0 ) => stderr_done = true ,
237+ Ok ( n) => stderr_buf. extend_from_slice( & stderr_chunk[ ..n] ) ,
238+ Err ( _) => stderr_done = true ,
239+ }
240+ }
241+ result = child. wait( ) , if exit_status. is_none( ) => {
242+ exit_status = Some ( result. unwrap( ) ) ;
243+ }
244+ _ = & mut timeout, if !timed_out => {
245+ // Timeout - kill the process
246+ let _ = child. kill( ) . await ;
247+ timed_out = true ;
248+ }
249+ }
250+
251+ // Exit conditions:
252+ // 1. Process exited and all output drained
253+ // 2. Timed out and all output drained (after kill, pipes close)
254+ if ( exit_status. is_some ( ) || timed_out) && stdout_done && stderr_done {
255+ break ;
256+ }
257+ }
258+
259+ // Format output
260+ if timed_out {
261+ e2e_outputs. push_str ( "[timeout]" ) ;
262+ } else if let Some ( status) = exit_status {
263+ let exit_code = status. code ( ) . unwrap_or ( -1 ) ;
264+ if exit_code != 0 {
265+ e2e_outputs. push_str ( format ! ( "[{}]" , exit_code) . as_str ( ) ) ;
266+ }
267+ }
268+
193269 e2e_outputs. push_str ( "> " ) ;
194270 e2e_outputs. push_str ( step. cmd ( ) ) ;
195271 e2e_outputs. push ( '\n' ) ;
196272
197- let stdout = String :: from_utf8 ( output . stdout ) . unwrap ( ) ;
198- let stderr = String :: from_utf8 ( output . stderr ) . unwrap ( ) ;
273+ let stdout = String :: from_utf8_lossy ( & stdout_buf ) . into_owned ( ) ;
274+ let stderr = String :: from_utf8_lossy ( & stderr_buf ) . into_owned ( ) ;
199275 e2e_outputs. push_str ( & redact_e2e_output ( stdout, e2e_stage_path_str) ) ;
200276 e2e_outputs. push_str ( & redact_e2e_output ( stderr, e2e_stage_path_str) ) ;
201277 e2e_outputs. push ( '\n' ) ;
278+
279+ // Skip remaining steps if timed out
280+ if timed_out {
281+ break ;
282+ }
202283 }
203284 insta:: assert_snapshot!( e2e. name. as_str( ) , e2e_outputs) ;
204285 }
@@ -212,9 +293,10 @@ fn main() {
212293
213294 let tests_dir = std:: env:: current_dir ( ) . unwrap ( ) . join ( "tests" ) ;
214295
215- insta:: glob!( tests_dir, "e2e_snapshots/fixtures/*" , |case_path| run_case(
216- & tmp_dir_path,
217- case_path,
218- filter. as_deref( )
219- ) ) ;
296+ // Create tokio runtime for async operations
297+ let runtime = tokio:: runtime:: Runtime :: new ( ) . unwrap ( ) ;
298+
299+ insta:: glob!( tests_dir, "e2e_snapshots/fixtures/*" , |case_path| {
300+ run_case( & runtime, & tmp_dir_path, case_path, filter. as_deref( ) )
301+ } ) ;
220302}
0 commit comments