diff --git a/Cargo.lock b/Cargo.lock index d18f72928..9e3103998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,6 +1125,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -4986,6 +4992,7 @@ dependencies = [ "cow-utils", "cp_r", "ctrlc", + "dunce", "jsonc-parser", "libc", "libtest-mimic", diff --git a/Cargo.toml b/Cargo.toml index ebd5a3d14..4fe61fa87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ ctrlc = "3.5.2" derive_more = "2.0.1" diff-struct = "0.5.3" directories = "6.0.0" +dunce = "1.0.5" elf = { version = "0.8.0", default-features = false } flate2 = "1.0.35" fspy = { path = "crates/fspy" } diff --git a/crates/vt_bin/Cargo.toml b/crates/vt_bin/Cargo.toml index 1b4e4e0fc..0b39cb66a 100644 --- a/crates/vt_bin/Cargo.toml +++ b/crates/vt_bin/Cargo.toml @@ -38,6 +38,7 @@ nix = { workspace = true, features = ["mount", "sched", "user"] } cargo-platform = { workspace = true } cow-utils = { workspace = true } cp_r = { workspace = true } +dunce = { workspace = true } libtest-mimic = { workspace = true } vt_snapshot_test = { workspace = true } vt_pty_terminal = { workspace = true } diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots.toml index afacfc8e5..4a8c33e08 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots.toml @@ -6,10 +6,10 @@ A task that runs `pnpm run` reads pnpm's `node_modules/.pnpm-workspace-state-v1. ignore = true steps = [ { argv = [ - "vt", - "run", - "--no-cache", - "pnpm-install", + "pnpm", + "install", + "--frozen-lockfile", + "--silent", ], comment = "install, recording pnpm's dependency check" }, { argv = [ "vtt", @@ -42,11 +42,11 @@ steps = [ "vtt print hello", ], comment = "switch back: package.json is unchanged but newer than pnpm's last check" }, { argv = [ - "vt", + "pnpm", + "--silent", "run", - "--no-cache", - "pnpm-greet", - ], comment = "an uncached run: pnpm rechecks dependencies and rewrites the state file" }, + "greet", + ], comment = "pnpm rechecks dependencies and rewrites the state file" }, { argv = [ "node", "compare-state.mjs", diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots/pnpm_workspace_state_ignored.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots/pnpm_workspace_state_ignored.md index f05bc686c..62159bd25 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots/pnpm_workspace_state_ignored.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/snapshots/pnpm_workspace_state_ignored.md @@ -2,12 +2,11 @@ A task that runs `pnpm run` reads pnpm's `node_modules/.pnpm-workspace-state-v1.json`, which pnpm rewrites with a new timestamp whenever it rechecks dependencies. The file is not part of the fingerprint, so the task keeps its cache hit after another pnpm command rechecks. -## `vt run --no-cache pnpm-install` +## `pnpm install --frozen-lockfile --silent` install, recording pnpm's dependency check ``` -$ pnpm install --frozen-lockfile --silent ⊘ cache disabled ``` ## `vtt cp node_modules/.pnpm-workspace-state-v1.json state-after-install.json` @@ -52,12 +51,11 @@ switch back: package.json is unchanged but newer than pnpm's last check ``` ``` -## `vt run --no-cache pnpm-greet` +## `pnpm --silent run greet` -an uncached run: pnpm rechecks dependencies and rewrites the state file +pnpm rechecks dependencies and rewrites the state file ``` -$ pnpm --silent run greet ⊘ cache disabled hello ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/vite-task.json index 3f26e3c47..3d09d33f9 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/pnpm_workspace_state/vite-task.json @@ -1,8 +1,5 @@ { "tasks": { - "pnpm-install": { - "command": "pnpm install --frozen-lockfile --silent" - }, "pnpm-greet": { "command": "pnpm --silent run greet", "cache": true diff --git a/crates/vt_bin/tests/e2e_snapshots/main.rs b/crates/vt_bin/tests/e2e_snapshots/main.rs index 345c3ee7b..0e4d1c819 100644 --- a/crates/vt_bin/tests/e2e_snapshots/main.rs +++ b/crates/vt_bin/tests/e2e_snapshots/main.rs @@ -464,17 +464,29 @@ fn run_case( let argv = step.argv(); let program = argv[0].as_str(); + let step_cwd = e2e_stage_path.join(step.cwd().unwrap_or(&e2e.cwd)); // Resolve the locally built task binaries exactly. Other programs // are resolved through the prepared PATH, allowing ignored tests // to opt into external toolchains without weakening the default - // suite's self-contained execution. - let mut cmd = if matches!(program, "vt" | "vtt") { + // suite's self-contained execution. They're resolved like `vp` + // resolves task programs: portable-pty's own search on Windows + // prefers an exact name over `PATHEXT` extensions, so it would + // pick an extensionless shell shim such as `node_modules/.bin/pnpm` + // over `pnpm.cmd`, which Windows can't execute. + // + // `canonicalize` gives the staged workspace a verbatim (`\\?\`) + // path on Windows. `cmd.exe`, which runs npm-style `.cmd` shims + // such as pnpm's, rejects that as a working directory, so other + // programs start in its simplified form. + let (mut cmd, spawn_cwd) = if matches!(program, "vt" | "vtt") { let exe_env = vt_str::format!("CARGO_BIN_EXE_{program}"); let resolved = env::var_os(exe_env.as_str()).unwrap_or_else(|| panic!("{exe_env} not set")); - CommandBuilder::new(resolved) + (CommandBuilder::new(resolved), step_cwd.as_path()) } else { - CommandBuilder::new(program) + let resolved = which::which_in(program, Some(&e2e_env_path), step_cwd.as_path()) + .unwrap_or_else(|err| panic!("failed to resolve {program}: {err}")); + (CommandBuilder::new(resolved), dunce::simplified(step_cwd.as_path())) }; for arg in &argv[1..] { cmd.arg(arg.as_str()); @@ -526,8 +538,7 @@ fn run_case( let resolved = resolve_env_placeholder(v.as_str()); cmd.env(k.as_str(), AsRef::::as_ref(&resolved)); } - let step_cwd = step.cwd().unwrap_or(&e2e.cwd); - cmd.cwd(e2e_stage_path.join(step_cwd).as_path()); + cmd.cwd(spawn_cwd); let terminal = TestTerminal::spawn(SCREEN_SIZE, cmd).unwrap(); let mut killer = terminal.child_handle.clone();