Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1259,8 +1259,15 @@ jobs:
# Generate the overrides from the fixtures, including automatic Ctrl-C
# isolation, so this configuration cannot drift from the runner.
VP_SNAP_NEXTEST_CONFIG="$RUNNER_TEMP/snapshot-nextest.toml" \
VP_SNAP_PREWARM_MANIFEST="$RUNNER_TEMP/snapshot-prewarm.json" \
cargo-nextest nextest list --archive-file windows-snapshot-tests.tar.zst --workspace-remap . \
> "$RUNNER_TEMP/snapshot-test-list.txt"
# Derive Bun archives from the same fixtures as the test list, so
# version changes need no matching CI edit. Only response bytes are shared.
node packages/tools/src/local-npm-registry.ts \
--snapshot-cache "$VP_SNAP_REGISTRY_CACHE_DIR" \
--request-log "$VP_SNAP_ARTIFACTS_DIR/prewarm/registry.jsonl" \
--prewarm-manifest "$RUNNER_TEMP/snapshot-prewarm.json"
nextest_run_args=(
--test-threads 8 --config-file "$RUNNER_TEMP/snapshot-nextest.toml"
--archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast
Expand Down
23 changes: 23 additions & 0 deletions crates/vp_cli_snapshots/tests/cli_snapshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Environment overrides, mainly for CI:
| `VP_SNAP_PACKAGES_DIR` | Run-scoped directory for sharing packed packages across test processes |
| `VP_SNAP_ARTIFACTS_DIR` | Directory for phase timings and failure diagnostics; unset disables artifacts |
| `VP_SNAP_NEXTEST_CONFIG` | With `--list`, write nextest overrides for the discovered isolated cases |
| `VP_SNAP_PREWARM_MANIFEST` | With `--list`, write a JSON array of Bun archive URLs derived from runnable cases |

Windows CI generates its nextest configuration from the same case definitions.
Exact nextest runs read only the selected fixture; listing and native shard
Expand Down Expand Up @@ -140,6 +141,27 @@ including across nextest processes when `VP_SNAP_REGISTRY_CACHE_DIR` is set.
Use a fresh directory per run; this is not a persistent dependency cache.
Package-manager caches and installed packages remain isolated per case.

Windows CI generates a prewarm manifest while listing cases, then passes it to
`local-npm-registry.ts --prewarm-manifest <file>` before starting test commands.
The runner collects exact Bun pins from each case's package ancestry
(`packageManager` and `devEngines.packageManager`), case/step environment overrides,
`vpt json-edit` of package-manager declarations, `vpt write-file` of package files,
and `vp env default/use/exec` arguments. Versions are deduplicated and archives
match the runner platform, including the x64 baseline variant. Changing these
fixture pins automatically updates preparation; CI contains no separate version list.

Ignored and platform-excluded cases do not contribute pins. Set `prewarm-bun = false`
for fake installations and configuration-only cases. Script-generated pins must use
declarative setup or environment overrides to participate; discovery does not execute
scripts or scan assertions. Floating tags and version ranges retain normal registry
resolution and are not prewarmed. The manifest includes all runnable cases before
nextest partitions them, so each shard can reuse the same preparation logic.

Preparation retries
failed downloads and permits up to 180 seconds per attempt. Tests reuse the
cached response with their existing deadlines and isolated installations.
Preparation requests are logged in `prewarm/registry.jsonl` under the artifact root.

`local-registry = true` additionally serves the packed checkout packages.
Published versions remain available for cases that deliberately install an older
release. Their upstream metadata is fetched once per run and merged with the
Expand Down Expand Up @@ -216,6 +238,7 @@ requires = ["bash"] # "sh" | "bash" | "zsh" | "cmd" | "fish" | "nu" |
ignore = false # true: only runs with `-- --ignored`
serial = false # true: run alone across cargo test/nextest workers
seed-runtime = true # false: start from an empty VP_HOME
prewarm-bun = true # false: fake install or configuration-only case
link-node-modules = false # true: expose the run-root node_modules as
# the workspace's parent-dir node_modules,
# for `../node_modules/vite-plus/...` paths
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[[case]]
name = "command_env_package_manager_modes"
prewarm-bun = false # Inspect configuration without downloading Bun.
vp = "global"
seed-runtime = false
steps = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"-/npm/v1/security/advisories/bulk": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ name = "pm_audit_bun"
skip-platforms = ["windows"]
vp = "global"
cwd = "bun"
# mock-manifest.json keeps audit responses local so these command-routing checks
# do not depend on the availability or contents of npm's advisory service.
steps = [
{ argv = ["vp", "install", "--", "--silent"], comment = "should install packages first", continue-on-failure = true },
{ argv = ["vp", "pm", "audit", "--production"], comment = "should audit production dependencies" },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[[case]]
name = "pm_ci_package_managers"
prewarm-bun = false # This case installs fake package managers.
vp = "global"
comment = """
Covers `vp pm ci` command delegation for each supported package manager.
Expand Down
32 changes: 32 additions & 0 deletions crates/vp_cli_snapshots/tests/cli_snapshots/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#[path = "../../src/bin/vpt/exit_code.rs"]
mod exit_code;
mod flavor;
mod prewarm;
mod redact;
mod registry_pack;
mod report;
Expand Down Expand Up @@ -389,6 +390,9 @@ struct Case {
/// Keep registry request counters private for tests of metadata caching.
#[serde(default, rename = "isolated-registry")]
isolated_registry: bool,
/// Disable archive preparation for fake installations and configuration-only cases.
#[serde(default = "default_true", rename = "prewarm-bun")]
prewarm_bun: bool,
/// Seed the case's `VP_HOME` with an already-provisioned managed JS
/// runtime (see `flavor::js_runtime_seed_dir`). Default true; runtime
/// provisioning tests set false to start from a genuinely empty home.
Expand Down Expand Up @@ -1972,6 +1976,9 @@ fn main() {

let mut tests: Vec<libtest_mimic::Trial> = Vec::new();
let mut isolated_trials = BTreeSet::new();
let prewarm_manifest =
args.list.then(|| std::env::var_os("VP_SNAP_PREWARM_MANIFEST")).flatten();
let mut bun_prewarm = prewarm::BunPrewarm::default();
for fixture_path in fixture_paths {
let fixture_path: Arc<Path> = Arc::from(fixture_path.as_path());
let fixture_name: Arc<str> = Arc::from(fixture_path.file_name().unwrap().to_str().unwrap());
Expand Down Expand Up @@ -2007,6 +2014,24 @@ fn main() {
let ignored = case.ignore
|| required_tool_missing
|| (case.local_registry && !local_build_present);
if prewarm_manifest.is_some() && !ignored && case.prewarm_bun {
bun_prewarm
.env(case.env.iter().map(|(key, value)| (key.as_str(), value.as_str())));
bun_prewarm.workspace(&fixture_path, &case.cwd).unwrap_or_else(|error| {
panic!("failed to read prewarm inputs for {trial_name}: {error}")
});
for step in case.steps.iter().chain(&case.after) {
bun_prewarm
.workspace(&fixture_path, step.cwd.as_deref().unwrap_or(&case.cwd))
.unwrap_or_else(|error| {
panic!("failed to read prewarm inputs for {trial_name}: {error}")
});
bun_prewarm.env(
step.envs.iter().map(|(key, value)| (key.as_str(), value.as_str())),
);
bun_prewarm.step(&step.argv);
}
}
let isolated = case_needs_isolation(&case);
if isolated {
isolated_trials.insert(trial_name.clone());
Expand Down Expand Up @@ -2086,6 +2111,13 @@ fn main() {
}
}

if let Some(path) = prewarm_manifest {
let urls = bun_prewarm.urls(&prewarm::platform_package());
std::fs::write(&path, serde_json::to_vec_pretty(&urls).unwrap()).unwrap_or_else(|error| {
panic!("failed to write prewarm manifest {}: {error}", Path::new(&path).display())
});
}

if args.list
&& let Some(path) = std::env::var_os("VP_SNAP_NEXTEST_CONFIG")
{
Expand Down
141 changes: 141 additions & 0 deletions crates/vp_cli_snapshots/tests/cli_snapshots/prewarm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::{
collections::BTreeSet,
path::{Path, PathBuf},
sync::LazyLock,
};

use serde_json::Value;

/// Exact fixture pins only. Floating versions keep their normal registry resolution.
static EXACT_VERSION: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+[0-9A-Za-z.-]+)?$").unwrap()
});

#[derive(Default)]
pub struct BunPrewarm {
versions: BTreeSet<String>,
packages: BTreeSet<PathBuf>,
}

impl BunPrewarm {
fn version(&mut self, spec: &str) {
if let Some(version) = EXACT_VERSION.captures(spec).and_then(|captures| captures.get(1)) {
self.versions.insert(version.as_str().to_owned());
}
}

fn package_manager(&mut self, value: &Value) {
if let Some(spec) = value.as_str().and_then(|value| value.strip_prefix("bun@")) {
self.version(spec);
}
}

fn dev_engine(&mut self, value: &Value) {
if let Some(entries) = value.as_array() {
for entry in entries {
self.dev_engine(entry);
}
} else if value["name"] == "bun"
&& let Some(version) = value["version"].as_str()
{
self.version(version);
}
}

pub fn package_json(&mut self, value: &Value) {
self.package_manager(&value["packageManager"]);
self.dev_engine(&value["devEngines"]["packageManager"]);
}

/// Read the same package ancestry a step sees, without scanning unrelated subprojects.
pub fn workspace(&mut self, fixture: &Path, cwd: &str) -> std::io::Result<()> {
let directory = fixture.join(cwd);
for ancestor in directory.ancestors().take_while(|path| path.starts_with(fixture)) {
let package = ancestor.join("package.json");
if !self.packages.insert(package.clone()) {
continue;
}
match std::fs::read_to_string(package) {
Ok(text) => {
// Malformed package files are deliberate inputs in validation cases.
if let Ok(value) = serde_json::from_str(&text) {
self.package_json(&value);
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
}
Ok(())
}

pub fn env<'a>(&mut self, entries: impl IntoIterator<Item = (&'a str, &'a str)>) {
for (key, value) in entries {
match key {
"VP_BUN_VERSION" => self.version(value),
"VP_PACKAGE_MANAGER" => self.package_manager(&Value::String(value.to_owned())),
_ => {}
}
}
}

/// Inspect declarative setup, not comments, assertions, or arbitrary script source.
pub fn step(&mut self, argv: &[String]) {
let args: Vec<&str> = argv.iter().map(String::as_str).collect();
match args.as_slice() {
["vpt", "json-edit", file, key, raw]
if Path::new(file).file_name().is_some_and(|name| name == "package.json") =>
{
let value =
serde_json::from_str(raw).unwrap_or_else(|_| Value::String((*raw).into()));
match *key {
"packageManager" => self.package_manager(&value),
"devEngines" => self.dev_engine(&value["packageManager"]),
"devEngines.packageManager" => self.dev_engine(&value),
_ => {}
}
}
["vpt", "write-file", file, text]
if Path::new(file).file_name().is_some_and(|name| name == "package.json") =>
{
if let Ok(value) = serde_json::from_str(text) {
self.package_json(&value);
}
}
["vp", "env", "default" | "use" | "exec", specs @ ..] => {
for spec in specs {
self.package_manager(&Value::String((*spec).into()));
}
}
_ => {}
}
}

pub fn urls(&self, package: &str) -> Vec<String> {
self.versions
.iter()
.map(|version| {
format!("https://registry.npmjs.org/@oven/{package}/-/{package}-{version}.tgz")
})
.collect()
}
}

/// Match the native Bun archive selected by vp_pm_cli, including older x64 CPUs.
pub fn platform_package() -> String {
let platform = match (std::env::consts::OS, std::env::consts::ARCH) {
("windows", "x86_64") => "windows-x64",
("windows", "aarch64") => "windows-aarch64",
("macos", "x86_64") => "darwin-x64",
("macos", "aarch64") => "darwin-aarch64",
("linux", "x86_64") => "linux-x64",
("linux", "aarch64") => "linux-aarch64",
(os, arch) => panic!("unsupported Bun prewarm platform: {os}-{arch}"),
};
let libc = if cfg!(all(target_os = "linux", target_env = "musl")) { "-musl" } else { "" };
#[cfg(target_arch = "x86_64")]
let baseline = if std::arch::is_x86_feature_detected!("avx2") { "" } else { "-baseline" };
#[cfg(not(target_arch = "x86_64"))]
let baseline = "";
format!("bun-{platform}{libc}{baseline}")
}
Loading
Loading