@@ -64,16 +64,48 @@ enum Step {
6464#[ derive( serde:: Deserialize , Debug ) ]
6565#[ serde( deny_unknown_fields) ]
6666struct StepConfig {
67- command : Str ,
67+ /// Shell command string (run via `sh -c`).
68+ #[ serde( default ) ]
69+ command : Option < Str > ,
70+ /// Argument vector — spawned directly without a shell wrapper.
71+ #[ serde( default ) ]
72+ argv : Option < Vec < Str > > ,
6873 #[ serde( default ) ]
6974 interactions : Vec < Interaction > ,
7075}
7176
77+ /// How to spawn a step: either via shell or directly.
78+ enum StepSpawn < ' a > {
79+ /// Run through `sh -c "<command>"`.
80+ Shell ( & ' a str ) ,
81+ /// Spawn directly with the given argv (first element is the program).
82+ Direct ( & ' a [ Str ] ) ,
83+ }
84+
7285impl Step {
73- fn command ( & self ) -> & str {
86+ fn spawn_mode ( & self ) -> StepSpawn < ' _ > {
7487 match self {
75- Self :: Command ( command) => command. as_str ( ) ,
76- Self :: Detailed ( config) => config. command . as_str ( ) ,
88+ Self :: Command ( command) => StepSpawn :: Shell ( command. as_str ( ) ) ,
89+ Self :: Detailed ( config) => config. argv . as_deref ( ) . map_or_else (
90+ || {
91+ StepSpawn :: Shell (
92+ config
93+ . command
94+ . as_ref ( )
95+ . expect ( "step must have either 'command' or 'argv'" )
96+ . as_str ( ) ,
97+ )
98+ } ,
99+ StepSpawn :: Direct ,
100+ ) ,
101+ }
102+ }
103+
104+ #[ expect( clippy:: disallowed_types, reason = "String required by join" ) ]
105+ fn display_command ( & self ) -> String {
106+ match self . spawn_mode ( ) {
107+ StepSpawn :: Shell ( cmd) => cmd. to_string ( ) ,
108+ StepSpawn :: Direct ( argv) => argv. iter ( ) . map ( Str :: as_str) . collect :: < Vec < _ > > ( ) . join ( " " ) ,
77109 }
78110 }
79111
@@ -187,9 +219,11 @@ fn run_case(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, filter: Optio
187219 {
188220 println ! ( "{fixture_name}" ) ;
189221 }
190- // Configure insta to write snapshots to fixture directory
222+ // Use the staged copy's snapshot dir so insta can write .snap.new files.
223+ // The original fixture_path may be on a read-only filesystem (e.g. WebDAV).
224+ let stage_snapshot_path = tmpdir. as_path ( ) . join ( fixture_name) . join ( "snapshots" ) ;
191225 let mut settings = insta:: Settings :: clone_current ( ) ;
192- settings. set_snapshot_path ( fixture_path . join ( "snapshots" ) ) ;
226+ settings. set_snapshot_path ( stage_snapshot_path ) ;
193227 settings. set_prepend_module_to_snapshot ( false ) ;
194228 settings. remove_snapshot_suffix ( ) ;
195229
@@ -276,10 +310,27 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
276310
277311 let mut e2e_outputs = String :: new ( ) ;
278312 for step in & e2e. steps {
279- let step_command = step. command ( ) ;
280- let mut cmd = CommandBuilder :: new ( & shell_exe) ;
281- cmd. arg ( "-c" ) ;
282- cmd. arg ( step_command) ;
313+ let step_display = step. display_command ( ) ;
314+ let mut cmd = match step. spawn_mode ( ) {
315+ StepSpawn :: Shell ( command) => {
316+ let mut cmd = CommandBuilder :: new ( & shell_exe) ;
317+ cmd. arg ( "-c" ) ;
318+ cmd. arg ( command) ;
319+ cmd
320+ }
321+ StepSpawn :: Direct ( argv) => {
322+ // Resolve the program from CARGO_BIN_EXE_<name> if available,
323+ // since CommandBuilder doesn't do PATH lookup on all platforms.
324+ let program = argv[ 0 ] . as_str ( ) ;
325+ let exe_env = vite_str:: format!( "CARGO_BIN_EXE_{program}" ) ;
326+ let resolved = env:: var_os ( exe_env. as_str ( ) ) . unwrap_or_else ( || program. into ( ) ) ;
327+ let mut cmd = CommandBuilder :: new ( resolved) ;
328+ for arg in & argv[ 1 ..] {
329+ cmd. arg ( arg. as_str ( ) ) ;
330+ }
331+ cmd
332+ }
333+ } ;
283334 cmd. env_clear ( ) ;
284335 cmd. env ( "PATH" , & e2e_env_path) ;
285336 cmd. env ( "NO_COLOR" , "1" ) ;
@@ -390,7 +441,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
390441 }
391442
392443 e2e_outputs. push_str ( "> " ) ;
393- e2e_outputs. push_str ( step_command ) ;
444+ e2e_outputs. push_str ( & step_display ) ;
394445 e2e_outputs. push ( '\n' ) ;
395446
396447 e2e_outputs. push_str ( & redact_e2e_output ( output, e2e_stage_path_str) ) ;
@@ -412,9 +463,6 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
412463}
413464
414465fn main ( ) {
415- // SAFETY: Called before any threads are spawned; insta reads this lazily on first assertion.
416- unsafe { std:: env:: set_var ( "INSTA_REQUIRE_FULL_MATCH" , "1" ) } ;
417-
418466 let filter = std:: env:: args ( ) . nth ( 1 ) ;
419467
420468 let tmp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
@@ -430,7 +478,7 @@ fn main() {
430478
431479 // Copy .node-version to the tmp dir so version manager shims can resolve the correct
432480 // Node.js binary when running task commands.
433- let repo_root = manifest_dir. join ( "../.." ) . canonicalize ( ) . unwrap ( ) ;
481+ let repo_root = manifest_dir. parent ( ) . unwrap ( ) . parent ( ) . unwrap ( ) ;
434482 std:: fs:: copy ( repo_root. join ( ".node-version" ) , tmp_dir. path ( ) . join ( ".node-version" ) )
435483 . unwrap ( ) ;
436484
0 commit comments