test(cli): run nine Windows-excluded e2e suites on Windows (#15097)
Nine files under `crates/cli/tests/suite/` carried a file-level `#![cfg(unix)]`, and the two `pnpm`-differential tests in `root.rs` and `bin.rs` carried a Windows `ignore`, so none of them ever compiled or ran on the Windows CI job. Four of the gates blamed "program not found" on spawning the external `pnpm`. That dates from pacquet's own CI, which installed pnpm through npm as a `pnpm.cmd` shim: Windows resolves a bare program name against `.exe` alone, so the spawn failed. The workflows have installed pnpm through `pnpm/setup` since 2026-05, and that action downloads pnpm's native `pnpm.exe`, which resolves. Nothing in those four files is unix-specific beyond the spawn. The other gates were harness details rather than platform behavior: - `repeat_install.rs` compared inode numbers to tell a file the install reused from one it replaced. It now takes a hard link as a witness and compares with `same_file`, which answers the same question on both platforms. - `multiple_importers.rs` asserted the executable bit on a linked bin. `bundled_dependencies.rs` already had a helper that checks the bit on Unix and the `.cmd` / `.ps1` launchers on Windows; it moves to the shared test utils and both files call it. - `pnpx_alias.rs` copied the binary under a name with no `.exe` suffix. - `ignore_workspace.rs` ran `pwd`, and `pipeline_watch.rs` ran `mkdir -p` and `cp`, from a package script. Both now use `node -e`, as `pipeline_cache.rs` already did. - `pipeline_cargo_cache.rs` ran its built probe binary by a path with no `.exe` suffix. Five files stay gated, each with a comment saying why: `interrupt.rs` sends POSIX signals, `run_recursive.rs` and `exec_recursive.rs` put POSIX shell bodies in nearly every package script, and `global_virtual_store.rs` and `hoisted_node_linker.rs` assert layouts shaped around symlinks. Un-gating those is the remainder of pnpm/pnpm#15089. Related to pnpm/pnpm#15089.
This commit is contained in:
1 parent
d8be2b85d1
commit
652dcc802b
17 files changed
+164
-98
No files matched your search
Generated
+1
@@ -6244,6 +6244,7 @@ dependencies = [
|
||||
"pnpm-workspace-state",
|
||||
"pnpr",
|
||||
"pnpr-fixtures",
|
||||
"same-file",
|
||||
"serde_json",
|
||||
"ssri",
|
||||
"tar",
|
||||
|
||||
@@ -438,7 +438,6 @@ pub fn read_lockfile(path: &Path) -> Lockfile {
|
||||
/// lockfiles the upstream tests use to stage wanted/current divergence.
|
||||
/// `new_ref` takes any `snapshots:` dependency shape (`100.0.0`,
|
||||
/// `link:packages/foo`, ...).
|
||||
#[cfg(unix)]
|
||||
pub fn repin_snapshot_dependency(
|
||||
lockfile_path: &Path,
|
||||
snapshot_key: &str,
|
||||
@@ -464,6 +463,30 @@ pub fn repin_snapshot_dependency(
|
||||
lockfile.save_to_path(lockfile_path).expect("write the rewritten lockfile");
|
||||
}
|
||||
|
||||
/// Assert that `shim` is a bin a caller could actually invoke.
|
||||
///
|
||||
/// What that takes differs per platform: Unix has the executable bit on
|
||||
/// the extensionless shim, while Windows has no such bit and relies on the
|
||||
/// `.cmd` / `.ps1` launchers written next to it.
|
||||
pub fn assert_bin_linked(shim: &Path) {
|
||||
assert!(shim.exists(), "the bin must be linked at {shim:?}");
|
||||
#[cfg(unix)]
|
||||
assert!(
|
||||
pnpm_testing_utils::fs::is_path_executable(shim),
|
||||
"the bin shim at {shim:?} must be executable",
|
||||
);
|
||||
#[cfg(windows)]
|
||||
for extension in ["cmd", "ps1"] {
|
||||
let launcher = shim.with_file_name(format!(
|
||||
"{}.{extension}",
|
||||
shim.file_name()
|
||||
.expect("bin shim has a file name")
|
||||
.to_string_lossy(),
|
||||
));
|
||||
assert!(launcher.exists(), "the bin shim at {shim:?} needs its {extension} launcher");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_success(output: &Output) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
@@ -489,7 +512,6 @@ pub fn ndjson_records(output: &Output) -> Vec<Value> {
|
||||
|
||||
/// The `name: "pnpm" / level: "info"` log pnpm's headless installer
|
||||
/// emits when it is entered with an up-to-date lockfile.
|
||||
#[cfg(unix)]
|
||||
#[must_use]
|
||||
pub fn has_up_to_date_log(records: &[Value]) -> bool {
|
||||
records
|
||||
|
||||
@@ -246,13 +246,8 @@ fn bin_walks_past_an_ecosystem_manifest() {
|
||||
|
||||
/// Differential parity from a workspace member, whose own `package.json`
|
||||
/// makes it the local prefix `bin` prints for. pacquet must match pnpm
|
||||
/// byte-for-byte. Windows-skipped because it spawns the external `pnpm`
|
||||
/// shim (see the `ignore` reason).
|
||||
/// byte-for-byte.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "spawns the external `pnpm` shim (`pnpm.cmd`); std::process::Command can't resolve it via PATHEXT"
|
||||
)]
|
||||
fn bin_matches_pnpm_from_a_workspace_subdir() {
|
||||
let CommandTempCwd { root, workspace, .. } = CommandTempCwd::init();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Ports of the TypeScript `bundledDependencies` install suite
|
||||
//! (`installing/deps-installer/test/install/bundledDependencies.ts`).
|
||||
|
||||
pub use _utils::{append_workspace_yaml_key, pacquet_in};
|
||||
pub use _utils::{append_workspace_yaml_key, assert_bin_linked, pacquet_in};
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
@@ -217,29 +217,6 @@ fn bundled_dependencies_survive_a_lockfile_rewrite() {
|
||||
drop((root, npmrc_info)); // cleanup
|
||||
}
|
||||
|
||||
/// A linked bin means something different per platform: Unix has the
|
||||
/// executable bit on the extensionless shim, while Windows has no such bit and
|
||||
/// instead relies on the `.cmd` / `.ps1` launchers written next to it. Assert
|
||||
/// whichever of the two actually makes the bin invocable on the host.
|
||||
fn assert_bin_linked(shim: &Path) {
|
||||
assert!(shim.exists(), "the bundled dependency's bin must be linked at {shim:?}");
|
||||
#[cfg(unix)]
|
||||
assert!(
|
||||
pnpm_testing_utils::fs::is_path_executable(shim),
|
||||
"the bundled dependency's bin shim at {shim:?} must be executable",
|
||||
);
|
||||
#[cfg(windows)]
|
||||
for extension in ["cmd", "ps1"] {
|
||||
let launcher = shim.with_file_name(format!(
|
||||
"{}.{extension}",
|
||||
shim.file_name()
|
||||
.expect("bin shim has a file name")
|
||||
.to_string_lossy(),
|
||||
));
|
||||
assert!(launcher.exists(), "the bin shim at {shim:?} needs its {extension} launcher");
|
||||
}
|
||||
}
|
||||
|
||||
fn read_wanted_lockfile(workspace: &Path) -> Lockfile {
|
||||
let text =
|
||||
fs::read_to_string(workspace.join(Lockfile::FILE_NAME)).expect("read pnpm-lock.yaml");
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
//!
|
||||
//! [#443]: https://github.com/pnpm/pacquet/pull/443
|
||||
|
||||
#![cfg(unix)] // pnpm CLI: 'program not found' on Windows runners.
|
||||
|
||||
pub use _utils::*;
|
||||
|
||||
use crate::_utils;
|
||||
@@ -923,9 +921,18 @@ fn should_add_extra_node_paths_to_command_shims() {
|
||||
|
||||
let shim = fs::read_to_string(workspace.join("node_modules/.bin/hello-world-js-bin"))
|
||||
.expect("read the command shim");
|
||||
// Inside a relocatable root the shim names the entry relative to its
|
||||
// own directory; on Windows `generate_sh_shim` writes the absolute
|
||||
// path `@zkochan/cmd-shim` writes instead.
|
||||
let hoisted_modules_entry = if cfg!(windows) {
|
||||
"/node_modules/.pnpm/node_modules"
|
||||
} else {
|
||||
"$basedir_abs/../.pnpm/node_modules"
|
||||
};
|
||||
assert!(
|
||||
shim.contains("$basedir_abs/../.pnpm/node_modules"),
|
||||
"the shim must extend NODE_PATH with the hidden hoisted modules dir, relative to itself:\n{shim}",
|
||||
shim.contains(hoisted_modules_entry),
|
||||
"the shim must extend NODE_PATH with the hidden hoisted modules dir \
|
||||
({hoisted_modules_entry}):\n{shim}",
|
||||
);
|
||||
|
||||
// The fresh install's own `packages:` rows must record `hasBin` —
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! `--ignore-workspace` and `--workspace-packages`: the two flags that
|
||||
//! change which workspace, if any, a command belongs to. The scripts run
|
||||
//! through pacquet's `sh -c` executor, so the file is gated to Unix.
|
||||
#![cfg(unix)]
|
||||
//! change which workspace, if any, a command belongs to.
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
@@ -107,11 +105,13 @@ fn workspace_packages_overrides_the_manifest_patterns() {
|
||||
"--config.verify-deps-before-run=false",
|
||||
"-r",
|
||||
"exec",
|
||||
"pwd",
|
||||
"node",
|
||||
"-e",
|
||||
"console.log(process.cwd())",
|
||||
]));
|
||||
let selected = stdout.lines().collect::<Vec<_>>();
|
||||
assert_eq!(selected.len(), 1, "only alfa should be selected: {stdout}");
|
||||
assert!(selected[0].ends_with("packages/alfa"), "wrong project selected: {stdout}");
|
||||
assert!(Path::new(selected[0]).ends_with("packages/alfa"), "wrong project selected: {stdout}");
|
||||
|
||||
drop(root);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
//! Signal handling under `pnpm run`: what a `SIGINT` at the terminal
|
||||
//! reaches, and what it leaves behind.
|
||||
//!
|
||||
//! Unix-only by subject, not by harness. The tests send POSIX signals to a
|
||||
//! process group of their own; Windows delivers console control events
|
||||
//! instead, which needs its own tests rather than a port of these.
|
||||
#![cfg(unix)]
|
||||
|
||||
use command_extra::CommandExtra;
|
||||
|
||||
@@ -10,13 +10,10 @@
|
||||
//! equivalent is `pnpm --filter <project> install` in a
|
||||
//! `pnpm-workspace.yaml` workspace.
|
||||
|
||||
#![cfg(unix)] // pnpm CLI: 'program not found' on Windows runners.
|
||||
|
||||
pub use _utils::*;
|
||||
|
||||
use crate::_utils;
|
||||
|
||||
use pnpm_testing_utils::fs::is_path_executable;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
|
||||
@@ -493,13 +490,13 @@ fn links_workspace_package_bin_into_dependent_project() {
|
||||
|
||||
fixture.run(["install"]);
|
||||
let bin_path = main_project.join("node_modules/.bin/hello");
|
||||
assert!(is_path_executable(&bin_path), "expected an executable bin at {bin_path:?}");
|
||||
assert_bin_linked(&bin_path);
|
||||
|
||||
fs::remove_dir_all(main_project.join("node_modules")).expect("remove main's node_modules");
|
||||
fs::remove_dir_all(fixture.workspace.join("node_modules")).expect("remove root node_modules");
|
||||
fixture.run(["install", "--frozen-lockfile"]);
|
||||
|
||||
assert!(is_path_executable(&bin_path), "the frozen reinstall must re-link the bin");
|
||||
assert_bin_linked(&bin_path);
|
||||
}
|
||||
|
||||
/// TS: `custom virtual store directory in a workspace with shared
|
||||
@@ -695,8 +692,16 @@ fn link_bin_of_workspace_project_created_by_lifecycle_script() {
|
||||
assert!(consumer.join("created-by-prepare").exists());
|
||||
}
|
||||
|
||||
/// A script that appends `label` and a newline to the shared order log.
|
||||
///
|
||||
/// The newline comes from `String.fromCharCode` rather than a `\n` escape:
|
||||
/// `sh -c` unescapes a backslash inside double quotes before Node sees it,
|
||||
/// while `cmd /d /s /c` passes it through, so an escape would mean two
|
||||
/// different things per platform.
|
||||
fn append_order_script(label: &str) -> String {
|
||||
format!(r#"node -e "require('fs').appendFileSync('../../order.txt', '{label}\\n')""#)
|
||||
format!(
|
||||
r#"node -e "require('fs').appendFileSync('../../order.txt', '{label}' + String.fromCharCode(10))""#,
|
||||
)
|
||||
}
|
||||
|
||||
/// TS: `dependencies of workspace projects are built during headless
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#![cfg(unix)]
|
||||
//! The pipeline's Cargo build cache: shared between worktrees, and
|
||||
//! rebuilt after the cache directory is deleted.
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pnpm_testing_utils::command_env::CommandTestExt;
|
||||
use std::{
|
||||
env::consts::EXE_SUFFIX,
|
||||
fs::{self, File, FileTimes},
|
||||
path::Path,
|
||||
process::Command,
|
||||
@@ -40,12 +42,23 @@ fn pnpm(root: &Path, cache: &Path, args: &[&str]) -> String {
|
||||
}
|
||||
|
||||
fn run_binary(root: &Path) -> String {
|
||||
let output = Command::new(root.join("target/debug/probe")).output().unwrap();
|
||||
let output =
|
||||
Command::new(root.join(format!("target/debug/probe{EXE_SUFFIX}"))).output().unwrap();
|
||||
assert!(output.status.success());
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
|
||||
/// Windows-skipped on a product gap this test is not the place to fix: the
|
||||
/// second worktree recompiles instead of restoring the shared Cargo build
|
||||
/// state. The run there places the cache under the 8.3 short form of the
|
||||
/// temporary directory (`RUNNER~1`) while the worktree carries the long
|
||||
/// one, which is the first thing to check. Tracked in
|
||||
/// <https://github.com/pnpm/pnpm/issues/15105>.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "pnpm/pnpm#15105: the shared Cargo build state is not restored across worktrees"
|
||||
)]
|
||||
fn cargo_state_is_shared_between_worktrees_and_survives_cache_deletion() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(temp.path()).unwrap();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Watch-agent integration tests: poll a git repository, build new
|
||||
//! revisions of a branch in a persistent checkout, and skip ticks with
|
||||
//! nothing new. The build scripts run through pacquet's `sh -c`
|
||||
//! executor, so the file is gated to Unix like the other run suites.
|
||||
#![cfg(unix)]
|
||||
//! nothing new.
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
@@ -42,7 +40,16 @@ fn agent_checkout(root: &Path) -> PathBuf {
|
||||
state_dir.join("checkout").join("demo")
|
||||
}
|
||||
|
||||
/// Windows-skipped on a product gap this test is not the place to fix: the
|
||||
/// tick reports the revision built and passed, and the checkout holds no
|
||||
/// task output afterwards. Tracked in
|
||||
/// <https://github.com/pnpm/pnpm/issues/15105>. The rest of the file runs
|
||||
/// everywhere.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "pnpm/pnpm#15105: the agent's build reports success but leaves no output in the checkout"
|
||||
)]
|
||||
fn watch_agent_builds_new_revisions_and_skips_quiet_ticks() {
|
||||
let root = tempfile::Builder::new()
|
||||
.prefix("pacquet-test-")
|
||||
@@ -56,7 +63,7 @@ fn watch_agent_builds_new_revisions_and_skips_quiet_ticks() {
|
||||
);
|
||||
fixture.write_file(
|
||||
"pkg/package.json",
|
||||
r#"{ "name": "pkg", "version": "1.0.0", "scripts": { "build": "mkdir -p out && cp src/index.txt out/index.txt" } }"#,
|
||||
r#"{ "name": "pkg", "version": "1.0.0", "scripts": { "build": "node -e \"const fs = require('fs'); fs.mkdirSync('out', { recursive: true }); fs.copyFileSync('src/index.txt', 'out/index.txt')\"" } }"#,
|
||||
);
|
||||
fixture.write_file("pkg/src/index.txt", "v1");
|
||||
// The lockfile the checkout's frozen install verifies against.
|
||||
@@ -104,7 +111,7 @@ fn watch_agent_builds_new_revisions_and_skips_quiet_ticks() {
|
||||
|
||||
fixture.write_file(
|
||||
"pkg/package.json",
|
||||
r#"{"name":"pkg","version":"1.0.0","scripts":{"build":"exit 1"}}"#,
|
||||
r#"{"name":"pkg","version":"1.0.0","scripts":{"build":"node -e \"process.exit(1)\""}}"#,
|
||||
);
|
||||
let failed = fixture.commit("failing build");
|
||||
for _ in 0..2 {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(unix)] // running this on windows result in 'program not found'
|
||||
|
||||
pub use _utils::*;
|
||||
|
||||
use crate::_utils;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Covers the `current_exe`-based `dlx` injection (`argv_with_alias_subcommand`)
|
||||
//! that pnpm relies on for the Windows `pnpx`/`pnx` hardlinks, exercised on Unix
|
||||
//! by copying the binary under the alias name — the same code runs everywhere.
|
||||
#![cfg(unix)]
|
||||
//! that pnpm relies on for the Windows `pnpx`/`pnx` hardlinks. The binary is
|
||||
//! copied under the alias name rather than linked, which reaches the same code
|
||||
//! on every platform.
|
||||
|
||||
use std::{fs, os::unix::fs::PermissionsExt, process::Command};
|
||||
use std::{env::consts::EXE_SUFFIX, fs, process::Command};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -12,9 +12,18 @@ fn launched_as_pnpx_injects_the_dlx_subcommand() {
|
||||
let pacquet = env!("CARGO_BIN_EXE_pnpm");
|
||||
|
||||
let dir = TempDir::new().expect("create temp dir");
|
||||
let pnpx = dir.path().join("pnpx");
|
||||
let pnpx = dir
|
||||
.path()
|
||||
.join(format!("pnpx{EXE_SUFFIX}"));
|
||||
fs::copy(pacquet, &pnpx).expect("copy the binary under the pnpx name");
|
||||
fs::set_permissions(&pnpx, fs::Permissions::from_mode(0o755)).expect("make pnpx executable");
|
||||
// Windows has no executable bit — the `.exe` suffix above is what
|
||||
// makes the copy runnable there.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&pnpx, fs::Permissions::from_mode(0o755))
|
||||
.expect("make pnpx executable");
|
||||
}
|
||||
|
||||
let via_pnpx = Command::new(&pnpx)
|
||||
.arg("--help")
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
//! again, and asserts the second install converges without rebuilding
|
||||
//! what was still valid.
|
||||
|
||||
#![cfg(unix)] // pnpm CLI: 'program not found' on Windows runners.
|
||||
|
||||
pub use _utils::*;
|
||||
|
||||
use crate::_utils;
|
||||
@@ -16,8 +14,9 @@ use command_extra::CommandExtra;
|
||||
use pnpm_testing_utils::{
|
||||
bin::{AddMockedRegistry, CommandTempCwd},
|
||||
fixtures::tarball_with_manifest,
|
||||
fs::SameFileWitness,
|
||||
};
|
||||
use std::{fs, os::unix::fs::MetadataExt, path::Path};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
/// `version` field of the `package.json` under `workspace/relative`.
|
||||
fn version_of(workspace: &Path, relative: &str) -> String {
|
||||
@@ -58,9 +57,15 @@ fn reinstalls_missing_packages_during_headless_install() {
|
||||
|
||||
let dep_location =
|
||||
workspace.join("node_modules/.pnpm/is-positive@1.0.0/node_modules/is-positive");
|
||||
// Resolve the path while it still exists: Windows hands the tests a
|
||||
// temporary directory under its 8.3 short name, and the reporter
|
||||
// names the long one.
|
||||
let resolved_dep_location = canonical_path(&dep_location);
|
||||
fs::remove_dir_all(&dep_location).expect("remove the virtual-store copy");
|
||||
fs::remove_file(workspace.join("node_modules/is-positive"))
|
||||
.expect("remove the direct-dep symlink");
|
||||
// `remove_dirent` rather than `remove_file`: the direct dep is a
|
||||
// junction on Windows, which `DeleteFileW` refuses.
|
||||
pnpm_fs::remove_dirent(&workspace.join("node_modules/is-positive"))
|
||||
.expect("remove the direct-dep link");
|
||||
|
||||
let second = pacquet_in(&workspace)
|
||||
.with_args(["install", "--frozen-lockfile", "--reporter=ndjson"])
|
||||
@@ -71,9 +76,14 @@ fn reinstalls_missing_packages_during_headless_install() {
|
||||
second_events.contains("pnpm:_broken_node_modules"),
|
||||
"the missing dir must be reported: {second_events}",
|
||||
);
|
||||
// The event is NDJSON, so the path arrives with its separators
|
||||
// escaped. Build the needle the way the reporter wrote it instead of
|
||||
// matching the raw path, which no Windows event would contain.
|
||||
let reported_dep_location =
|
||||
serde_json::to_string(&resolved_dep_location).expect("serialize the missing path");
|
||||
assert!(
|
||||
second_events.contains(dep_location.to_str().expect("utf-8 path")),
|
||||
"the event must carry the missing path",
|
||||
second_events.contains(reported_dep_location.trim_matches('"')),
|
||||
"the event must carry the missing path {reported_dep_location}: {second_events}",
|
||||
);
|
||||
assert_eq!(version_of(&workspace, "node_modules/is-positive"), "1.0.0");
|
||||
|
||||
@@ -264,7 +274,7 @@ fn available_packages_used_when_node_modules_not_clean() {
|
||||
|
||||
let foobarqar_manifest = workspace
|
||||
.join("node_modules/.pnpm/@pnpm.e2e+foobarqar@1.0.0/node_modules/@pnpm.e2e/foobarqar/package.json");
|
||||
let inode_before = fs::metadata(&foobarqar_manifest).expect("stat foobarqar").ino();
|
||||
let foobarqar_witness = SameFileWitness::take(&foobarqar_manifest, root.path());
|
||||
|
||||
fs::write(
|
||||
workspace.join("package.json"),
|
||||
@@ -296,9 +306,8 @@ fn available_packages_used_when_node_modules_not_clean() {
|
||||
.success();
|
||||
|
||||
assert!(workspace.join("node_modules/@pnpm.e2e/pkg-with-1-dep").exists());
|
||||
assert_eq!(
|
||||
fs::metadata(&foobarqar_manifest).expect("stat foobarqar").ino(),
|
||||
inode_before,
|
||||
assert!(
|
||||
foobarqar_witness.is_intact(),
|
||||
"the already-materialized package must be reused, not re-imported",
|
||||
);
|
||||
let refetched: Vec<String> = index_file_contents(&store_dir)
|
||||
@@ -802,7 +811,7 @@ fn repeat_hoisted_install_with_workspace_member_deps_is_up_to_date() {
|
||||
let AddMockedRegistry { mock_instance, .. } = npmrc_info;
|
||||
|
||||
let hoisted_manifest = install_hoisted_workspace_member(pacquet, &workspace);
|
||||
let inode_before = fs::metadata(&hoisted_manifest).expect("stat the hoisted dep").ino();
|
||||
let hoisted_witness = SameFileWitness::take(&hoisted_manifest, root.path());
|
||||
|
||||
let second = pacquet_in(&workspace)
|
||||
.with_arg("install")
|
||||
@@ -813,11 +822,7 @@ fn repeat_hoisted_install_with_workspace_member_deps_is_up_to_date() {
|
||||
second_output.contains("Already up to date"),
|
||||
"the repeat install must short-circuit: {second_output}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::metadata(&hoisted_manifest).expect("stat the hoisted dep").ino(),
|
||||
inode_before,
|
||||
"the second install must re-import nothing",
|
||||
);
|
||||
assert!(hoisted_witness.is_intact(), "the second install must re-import nothing");
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
@@ -874,7 +879,7 @@ fn repeat_hoisted_install_with_unchanged_local_tarball_is_up_to_date() {
|
||||
.success();
|
||||
let hoisted_manifest =
|
||||
workspace.join("node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep/package.json");
|
||||
let inode_before = fs::metadata(&hoisted_manifest).expect("stat the hoisted dep").ino();
|
||||
let hoisted_witness = SameFileWitness::take(&hoisted_manifest, root.path());
|
||||
|
||||
let second = pacquet_in(&workspace)
|
||||
.with_arg("install")
|
||||
@@ -885,11 +890,7 @@ fn repeat_hoisted_install_with_unchanged_local_tarball_is_up_to_date() {
|
||||
second_output.contains("Already up to date"),
|
||||
"the unchanged tarball must leave the fast path available: {second_output}",
|
||||
);
|
||||
assert_eq!(
|
||||
fs::metadata(&hoisted_manifest).expect("stat the hoisted dep").ino(),
|
||||
inode_before,
|
||||
"the second install must re-import nothing",
|
||||
);
|
||||
assert!(hoisted_witness.is_intact(), "the second install must re-import nothing");
|
||||
|
||||
drop((root, mock_instance));
|
||||
}
|
||||
|
||||
@@ -153,17 +153,7 @@ fn root_global_writes_warnings_to_stderr_so_stdout_stays_a_clean_path() {
|
||||
/// Differential parity: from a workspace subdirectory pnpm's `root` prints the
|
||||
/// cwd's `node_modules` (its `config.dir` is the cwd, not the workspace root).
|
||||
/// pacquet must print byte-identical output.
|
||||
///
|
||||
/// Skipped on Windows, where pnpm is installed as a `pnpm.cmd` shim and
|
||||
/// `std::process::Command` does not honor `PATHEXT`, so `Command::new("pnpm")`
|
||||
/// fails with "program not found" (the same reason `pnpm_compatibility.rs` and
|
||||
/// `hoist.rs` gate their pnpm-spawning tests). The three tests above spawn only
|
||||
/// `pacquet`, so they keep running on Windows.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "spawns the external `pnpm` shim (`pnpm.cmd`); std::process::Command can't resolve it via PATHEXT"
|
||||
)]
|
||||
fn root_matches_pnpm_from_a_workspace_subdir() {
|
||||
let CommandTempCwd { root, workspace, .. } = CommandTempCwd::init();
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pnpm_testing_utils::{
|
||||
|
||||
@@ -17,6 +17,7 @@ dunce = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
pipe-trait = { workspace = true }
|
||||
pnpr = { workspace = true }
|
||||
same-file = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
ssri = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
|
||||
@@ -2,9 +2,10 @@ use pipe_trait::Pipe;
|
||||
use pnpm_workspace_state::load_workspace_state;
|
||||
use std::{
|
||||
fs, io,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[must_use]
|
||||
@@ -73,6 +74,41 @@ pub fn is_path_executable(path: &Path) -> bool {
|
||||
mode & 0b001_001_001 != 0
|
||||
}
|
||||
|
||||
/// A record of which on-disk file a path named at one point in time, so a
|
||||
/// later check can tell a file an install reused from one it replaced.
|
||||
///
|
||||
/// The record is a hard link, taken in a directory outside the tree under
|
||||
/// test, and [`Self::is_intact`] compares the two paths with `same_file`.
|
||||
/// Unix could keep the inode number instead, but `std` exposes the Windows
|
||||
/// equivalent only behind an unstable feature.
|
||||
pub struct SameFileWitness {
|
||||
path: PathBuf,
|
||||
/// Owns the directory the link lives in, so the link goes away with
|
||||
/// the witness.
|
||||
dir: TempDir,
|
||||
}
|
||||
|
||||
impl SameFileWitness {
|
||||
/// Link `path` from a directory of its own under `witness_dir`, which
|
||||
/// has to be on the same filesystem as `path` and outside whatever the
|
||||
/// step under test rewrites.
|
||||
#[must_use]
|
||||
pub fn take(path: &Path, witness_dir: &Path) -> Self {
|
||||
let dir = TempDir::new_in(witness_dir).expect("create the witness directory");
|
||||
let link = dir.path().join("link");
|
||||
fs::hard_link(path, &link)
|
||||
.unwrap_or_else(|error| panic!("link {path:?} from {link:?}: {error}"));
|
||||
SameFileWitness { path: path.to_path_buf(), dir }
|
||||
}
|
||||
|
||||
/// Whether the path still names the file it named when the witness was
|
||||
/// taken.
|
||||
#[must_use]
|
||||
pub fn is_intact(&self) -> bool {
|
||||
same_file::is_same_file(&self.path, self.dir.path().join("link")).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The gap that separates two mtimes on every filesystem the tests run on.
|
||||
///
|
||||
/// A full second, because the coarsest supported filesystems (HFS+, ext4
|
||||
|
||||
Reference in new issue
Block a user