@@ -9,7 +9,6 @@ use std::{
99 time:: { Duration , Instant } ,
1010} ;
1111
12- use copy_dir:: copy_dir;
1312use pty_terminal:: { geo:: ScreenSize , terminal:: CommandBuilder } ;
1413use pty_terminal_test:: TestTerminal ;
1514use redact:: redact_e2e_output;
@@ -23,6 +22,9 @@ const STEP_TIMEOUT: Duration = Duration::from_secs(20);
2322/// Screen size for the PTY terminal. Large enough to avoid line wrapping.
2423const SCREEN_SIZE : ScreenSize = ScreenSize { rows : 500 , cols : 500 } ;
2524
25+ const COMPILE_TIME_CARGO_BIN_EXE_VP : & str = env ! ( "CARGO_BIN_EXE_vp" ) ;
26+ const COMPILE_TIME_CARGO_MANIFEST_DIR : & str = env ! ( "CARGO_MANIFEST_DIR" ) ;
27+
2628/// Get the shell executable for running e2e test steps.
2729/// On Unix, uses /bin/sh.
2830/// On Windows, uses BASH env var or falls back to Git Bash.
@@ -53,6 +55,65 @@ fn get_shell_exe() -> std::path::PathBuf {
5355 }
5456}
5557
58+ #[ expect(
59+ clippy:: disallowed_types,
60+ reason = "Path types required for runtime path remapping between compile and runtime roots"
61+ ) ]
62+ fn runtime_manifest_dir ( ) -> std:: path:: PathBuf {
63+ std:: env:: var_os ( "CARGO_MANIFEST_DIR" ) . map_or_else (
64+ || std:: path:: PathBuf :: from ( COMPILE_TIME_CARGO_MANIFEST_DIR ) ,
65+ std:: path:: PathBuf :: from,
66+ )
67+ }
68+
69+ #[ expect(
70+ clippy:: disallowed_types,
71+ reason = "Path types required for runtime path remapping between compile and runtime roots"
72+ ) ]
73+ fn relative_path_from ( path : & std:: path:: Path , base : & std:: path:: Path ) -> std:: path:: PathBuf {
74+ use std:: path:: { Component , PathBuf } ;
75+
76+ let path_components = path. components ( ) . collect :: < Vec < _ > > ( ) ;
77+ let base_components = base. components ( ) . collect :: < Vec < _ > > ( ) ;
78+
79+ let common_prefix_len = path_components
80+ . iter ( )
81+ . zip ( base_components. iter ( ) )
82+ . take_while ( |( path_comp, base_comp) | path_comp == base_comp)
83+ . count ( ) ;
84+
85+ let mut relative_path = PathBuf :: new ( ) ;
86+
87+ for base_comp in & base_components[ common_prefix_len..] {
88+ match base_comp {
89+ Component :: Normal ( _) | Component :: CurDir | Component :: ParentDir => {
90+ relative_path. push ( ".." ) ;
91+ }
92+ Component :: RootDir | Component :: Prefix ( _) => { }
93+ }
94+ }
95+
96+ for path_comp in & path_components[ common_prefix_len..] {
97+ relative_path. push ( path_comp. as_os_str ( ) ) ;
98+ }
99+
100+ relative_path
101+ }
102+
103+ #[ expect(
104+ clippy:: disallowed_types,
105+ reason = "Path types required for runtime path remapping between compile and runtime roots"
106+ ) ]
107+ fn resolve_runtime_vp_path ( ) -> AbsolutePathBuf {
108+ let compile_time_vp = std:: path:: Path :: new ( COMPILE_TIME_CARGO_BIN_EXE_VP ) ;
109+ let compile_time_manifest = std:: path:: Path :: new ( COMPILE_TIME_CARGO_MANIFEST_DIR ) ;
110+ let vp_relative_path = relative_path_from ( compile_time_vp, compile_time_manifest) ;
111+
112+ let runtime_manifest = runtime_manifest_dir ( ) ;
113+ let runtime_vp = runtime_manifest. join ( vp_relative_path) ;
114+ AbsolutePathBuf :: new ( runtime_vp) . unwrap ( )
115+ }
116+
56117const fn default_true ( ) -> bool {
57118 true
58119}
@@ -207,6 +268,29 @@ enum TerminationState {
207268 TimedOut ,
208269}
209270
271+ #[ expect(
272+ clippy:: disallowed_types,
273+ reason = "Path required for recursive fixture copy in test harness"
274+ ) ]
275+ fn copy_dir_recursive ( src : & std:: path:: Path , dst : & std:: path:: Path ) -> std:: io:: Result < ( ) > {
276+ std:: fs:: create_dir_all ( dst) ?;
277+
278+ for entry in std:: fs:: read_dir ( src) ? {
279+ let entry = entry?;
280+ let source_path = entry. path ( ) ;
281+ let target_path = dst. join ( entry. file_name ( ) ) ;
282+ let file_type = entry. file_type ( ) ?;
283+
284+ if file_type. is_dir ( ) {
285+ copy_dir_recursive ( & source_path, & target_path) ?;
286+ } else if file_type. is_file ( ) {
287+ std:: fs:: copy ( & source_path, & target_path) ?;
288+ }
289+ }
290+
291+ Ok ( ( ) )
292+ }
293+
210294fn kill_process ( child : & mut std:: process:: Child ) {
211295 let _ = child. kill ( ) ;
212296}
@@ -222,7 +306,7 @@ fn kill_process(child: &mut std::process::Child) {
222306fn run_case_inner ( tmpdir : & AbsolutePath , fixture_path : & std:: path:: Path , fixture_name : & str ) {
223307 // Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case.
224308 let stage_path = tmpdir. join ( fixture_name) ;
225- copy_dir ( fixture_path, & stage_path) . unwrap ( ) ;
309+ copy_dir_recursive ( fixture_path, stage_path. as_path ( ) ) . unwrap ( ) ;
226310
227311 let ( workspace_root, _cwd) = find_workspace_root ( & stage_path) . unwrap ( ) ;
228312
@@ -238,13 +322,9 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
238322 Err ( err) => panic ! ( "Failed to read cases.toml for fixture {fixture_name}: {err}" ) ,
239323 } ;
240324
241- // Navigate from CARGO_MANIFEST_DIR to packages/tools at the repo root
242- #[ expect(
243- clippy:: disallowed_types,
244- reason = "Path required for CARGO_MANIFEST_DIR path manipulation via env! macro"
245- ) ]
246- let repo_root =
247- std:: path:: Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) . parent ( ) . unwrap ( ) . parent ( ) . unwrap ( ) ;
325+ // Navigate from runtime CARGO_MANIFEST_DIR to packages/tools at the repo root.
326+ let repo_root = runtime_manifest_dir ( ) ;
327+ let repo_root = repo_root. parent ( ) . unwrap ( ) . parent ( ) . unwrap ( ) ;
248328 let test_bin_path = Arc :: < OsStr > :: from (
249329 repo_root. join ( "packages" ) . join ( "tools" ) . join ( "node_modules" ) . join ( ".bin" ) . into_os_string ( ) ,
250330 ) ;
@@ -257,7 +337,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
257337 [
258338 // Include vp binary path to PATH so that e2e tests can run "vp ..." commands.
259339 {
260- let vp_path = AbsolutePath :: new ( env ! ( "CARGO_BIN_EXE_vp" ) ) . unwrap ( ) ;
340+ let vp_path = resolve_runtime_vp_path ( ) ;
261341 let vp_dir = vp_path. parent ( ) . unwrap ( ) ;
262342 vp_dir. as_path ( ) . as_os_str ( ) . into ( )
263343 } ,
@@ -289,7 +369,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
289369
290370 let e2e_stage_path = tmpdir. join ( vite_str:: format!( "{fixture_name}_e2e_stage_{e2e_count}" ) ) ;
291371 e2e_count += 1 ;
292- assert ! ( copy_dir ( fixture_path, & e2e_stage_path) . unwrap ( ) . is_empty ( ) ) ;
372+ copy_dir_recursive ( fixture_path, e2e_stage_path. as_path ( ) ) . unwrap ( ) ;
293373
294374 let e2e_stage_path_str = e2e_stage_path. as_path ( ) . to_str ( ) . unwrap ( ) ;
295375
@@ -530,20 +610,20 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
530610 }
531611}
532612
533- #[ expect( clippy:: disallowed_types, reason = "Path required by insta::glob! macro callback" ) ]
534- #[ expect(
535- clippy:: disallowed_methods,
536- reason = "current_dir needed because insta::glob! requires std PathBuf"
537- ) ]
538613fn main ( ) {
539614 let filter = std:: env:: args ( ) . nth ( 1 ) ;
540615
541616 let tmp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
542- let tmp_dir_path = AbsolutePathBuf :: new ( tmp_dir. path ( ) . canonicalize ( ) . unwrap ( ) ) . unwrap ( ) ;
617+ let tmp_dir_path = AbsolutePathBuf :: new ( tmp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
543618
544- let tests_dir = std:: env:: current_dir ( ) . unwrap ( ) . join ( "tests" ) ;
619+ let fixtures_dir = runtime_manifest_dir ( ) . join ( "tests" ) . join ( "e2e_snapshots" ) . join ( "fixtures" ) ;
620+ let mut fixture_paths = std:: fs:: read_dir ( fixtures_dir)
621+ . unwrap ( )
622+ . map ( |entry| entry. unwrap ( ) . path ( ) )
623+ . collect :: < Vec < _ > > ( ) ;
624+ fixture_paths. sort ( ) ;
545625
546- insta :: glob! ( tests_dir , "e2e_snapshots/fixtures/*" , | case_path| {
626+ for case_path in & fixture_paths {
547627 run_case ( & tmp_dir_path, case_path, filter. as_deref ( ) ) ;
548- } ) ;
628+ }
549629}
0 commit comments