pmacs/tests/m7_review_acceptance.rs

931 lines
34 KiB
Rust

// tests/m7_review_acceptance.rs --- Post-M7 audit-fix acceptance.
//
//! Acceptance tests for the M7 close-out review findings (see the
//! commit message and `TRANSITION-M7.md` "Review-driven fixes"
//! section). Each test pins a contract that was either inert or
//! contradicted by docs at M7.11 release time:
//!
//! - High #1: `pmacs.packages.install` resolves and installs the
//! full dependency closure, not just the top-level spec.
//! - High #3: Frozen-policy resolve verifies content hashes.
//! (Covered in `m7_6_acceptance.rs` --- the fix lives here too
//! for organizational clarity, but isn't re-asserted in this
//! file.)
//! - High #5: bundled-package dir lives under XDG, not /tmp.
//! - Medium #6: `gitlab:` is wired and parses to gitlab.com URLs.
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use pmacs::lua::LuaHost;
use pmacs::lua_bindings::PackageInstallOverride;
use pmacs::packages::Address;
use tempfile::TempDir;
// ---------------------------------------------------------------------------
// Fixture: a package whose manifest declares one dependency
// ---------------------------------------------------------------------------
fn run_git(args: &[&OsStr]) {
let mut cmd = Command::new("git");
for a in args {
cmd.arg(a);
}
cmd.env("GIT_TERMINAL_PROMPT", "0");
cmd.env("LC_ALL", "C");
let out = cmd.output().expect("git spawn");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn file_url(p: &Path) -> String {
format!("file://{}", p.display())
}
/// Build a bare repo for a package whose `pmacs.toml` carries one
/// optional dependency line. `dep_addr` is the upstream URL form to
/// embed in the manifest's `[[dependencies]]` block; pass `None` to
/// build a leaf package.
fn make_pkg_with_dep(
name: &str,
dep_addr: Option<&str>,
dep_constraint: &str,
) -> (TempDir, PathBuf) {
let td = tempfile::tempdir().expect("tempdir");
let work = td.path().join("work");
let bare = td.path().join("upstream.git");
run_git(&[
OsStr::new("init"),
OsStr::new("--initial-branch=main"),
work.as_os_str(),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.email"),
OsStr::new("test@example.com"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.name"),
OsStr::new("Tester"),
]);
let mut manifest = format!(
"name = \"{name}\"\n\
version = \"1.0.0\"\n\
summary = \"M7 review fixture: {name}\"\n\
pmacs_required = \">= 0.1.0\"\n\
entry = \"init.lua\"\n\
exports = [\"{name}\"]\n"
);
if let Some(addr) = dep_addr {
use std::fmt::Write;
write!(
manifest,
"\n[[dependencies]]\naddress = \"git:{addr}\"\nversion = \"{dep_constraint}\"\n"
)
.expect("write into String never fails");
}
std::fs::write(work.join("pmacs.toml"), manifest).expect("write manifest");
std::fs::write(
work.join("init.lua"),
format!("return {{ name = '{name}', version = '1.0.0' }}\n"),
)
.expect("write entry");
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("add"),
OsStr::new("."),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("commit"),
OsStr::new("-m"),
OsStr::new("v1.0.0"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("tag"),
OsStr::new("v1.0.0"),
]);
run_git(&[
OsStr::new("clone"),
OsStr::new("--bare"),
work.as_os_str(),
bare.as_os_str(),
]);
(td, bare)
}
fn host_with_overrides() -> (LuaHost, TempDir, TempDir) {
let cache = tempfile::tempdir().expect("cache tempdir");
let user_root = tempfile::tempdir().expect("user-root tempdir");
let host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
(host, cache, user_root)
}
// ---------------------------------------------------------------------------
// Review High #1: install resolves transitive deps
// ---------------------------------------------------------------------------
#[test]
fn install_resolves_and_installs_manifest_dependencies() {
// Build dep first, then app referencing dep's bare repo.
let (_dep_td, dep_bare) = make_pkg_with_dep("review-dep", None, "");
let dep_addr = file_url(&dep_bare);
let (_app_td, app_bare) = make_pkg_with_dep("review-app", Some(&dep_addr), "^1.0.0");
let app_addr = file_url(&app_bare);
let (mut host, _c, _u) = host_with_overrides();
let script = format!(
r#"
pmacs.packages.install {{ "git:{app_addr}", version = "^1.0.0" }}
local roster = pmacs.packages.installed()
local names = {{}}
for _, p in ipairs(roster) do names[p.name] = true end
assert(names["review-app"], "top-level review-app must be installed")
assert(names["review-dep"], "transitive review-dep must be installed (review fix)")
-- And require() works for the dependency, not just the top-level.
local app = require("review-app")
assert(app.name == "review-app")
local dep = require("review-dep")
assert(dep.name == "review-dep")
return "ok"
"#
);
host.eval(Some("m7_review"), &script)
.unwrap_or_else(|e| panic!("install with deps failed: {e}"));
}
// ---------------------------------------------------------------------------
// Review High #5: bundled dir is XDG-rooted, not /tmp-rooted
// ---------------------------------------------------------------------------
#[test]
fn bundled_runtime_dir_lives_under_xdg_data_home_when_set() {
// We cannot mutate XDG_DATA_HOME from a safe-Rust test
// (`std::env::set_var` is unsafe in 2024). Instead, the test
// validates the *resolution policy* the implementation uses by
// running with the current environment and asserting the result
// is at least not under `/tmp/pmacs-builtin-v...` (the old
// shape that the audit flagged).
let p = pmacs::builtin_packages::bundled_runtime_dir();
let s = p.to_string_lossy();
assert!(
!s.starts_with("/tmp/pmacs-builtin-v"),
"bundled dir must not sit under /tmp/pmacs-builtin-v...; got {s}"
);
}
// ---------------------------------------------------------------------------
// Review High #2: install writes a lockfile + update reads it
// ---------------------------------------------------------------------------
#[test]
fn install_writes_pmacs_lock_at_install_root() {
let (_dep_td, dep_bare) = make_pkg_with_dep("review-lock-dep", None, "");
let dep_addr = file_url(&dep_bare);
let (_app_td, app_bare) = make_pkg_with_dep("review-lock-app", Some(&dep_addr), "^1.0.0");
let app_addr = file_url(&app_bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
let script = format!(
r#"
pmacs.packages.install {{ "git:{app_addr}", version = "^1.0.0" }}
"#
);
host.eval(Some("m7_review_lock"), &script)
.unwrap_or_else(|e| panic!("install failed: {e}"));
let lock_path = user_root.path().join("pmacs.lock");
assert!(
lock_path.exists(),
"expected lockfile at {lock_path:?} after install"
);
let lock = pmacs::packages::Lockfile::read_from(&lock_path).expect("parse lockfile");
let names: Vec<&str> = lock.packages.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"review-lock-app"),
"lockfile must list app: {names:?}"
);
assert!(
names.contains(&"review-lock-dep"),
"lockfile must list transitive dep: {names:?}"
);
}
#[test]
fn install_merges_existing_lockfile_across_calls() {
// Two top-level installs in the same Lua host should result in
// a lockfile that lists both. Without merge, the second call's
// lockfile would clobber the first call's.
let (_a_td, a_bare) = make_pkg_with_dep("review-merge-a", None, "");
let (_b_td, b_bare) = make_pkg_with_dep("review-merge-b", None, "");
let a_addr = file_url(&a_bare);
let b_addr = file_url(&b_bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
let script = format!(
r#"
pmacs.packages.install {{ "git:{a_addr}", version = "^1.0.0" }}
pmacs.packages.install {{ "git:{b_addr}", version = "^1.0.0" }}
"#
);
host.eval(Some("m7_review_merge"), &script)
.unwrap_or_else(|e| panic!("merged install failed: {e}"));
let lock_path = user_root.path().join("pmacs.lock");
let lock = pmacs::packages::Lockfile::read_from(&lock_path).expect("parse lockfile");
let names: Vec<&str> = lock.packages.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"review-merge-a") && names.contains(&"review-merge-b"),
"lockfile must contain both packages after sequential installs: {names:?}"
);
}
#[test]
fn update_without_lockfile_returns_clear_error() {
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
let err = host
.eval(Some("m7_review_update_empty"), "pmacs.packages.update()")
.expect_err("update without lockfile must error");
let msg = err.to_string();
assert!(
msg.contains("lockfile") || msg.contains("install"),
"error should mention missing lockfile or guide user to install: {msg}"
);
}
#[test]
fn update_unknown_package_name_returns_clear_error() {
// Need a baseline lockfile, so install one package first.
let (_td, bare) = make_pkg_with_dep("review-update-base", None, "");
let addr = file_url(&bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
let install_script =
format!(r#"pmacs.packages.install {{ "git:{addr}", version = "^1.0.0" }}"#);
host.eval(Some("install"), &install_script)
.unwrap_or_else(|e| panic!("baseline install failed: {e}"));
let err = host
.eval(
Some("update_unknown"),
"pmacs.packages.update(\"no-such-package\")",
)
.expect_err("update of unknown name must error");
let msg = err.to_string();
assert!(
msg.contains("no-such-package"),
"error must name the unknown package: {msg}"
);
}
#[test]
fn update_all_no_op_when_upstream_unchanged() {
// Install once, lockfile is written. update() with no changes
// upstream should produce a plan that re-resolves to the same
// commits.
let (_td, bare) = make_pkg_with_dep("review-update-a", None, "");
let addr = file_url(&bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
let script = format!(
r#"
pmacs.packages.install {{ "git:{addr}", version = "^1.0.0" }}
local summary = pmacs.packages.update()
assert(type(summary) == "table", "update should return a summary table")
assert(#summary >= 1, "summary should list at least one package")
local found = false
for _, row in ipairs(summary) do
if row.name == "review-update-a" then
found = true
assert(row.changed == false,
"no-op update should report changed = false; got " .. tostring(row.changed))
end
end
assert(found, "summary must include review-update-a")
"#
);
host.eval(Some("m7_review_update_noop"), &script)
.unwrap_or_else(|e| panic!("update no-op failed: {e}"));
}
// ---------------------------------------------------------------------------
// Review Medium #6: gitlab: address parses
// ---------------------------------------------------------------------------
#[test]
fn gitlab_address_parses_and_canonicalizes_to_gitlab_com() {
let a = Address::parse("gitlab:user/repo").expect("gitlab: should parse");
assert!(matches!(a, Address::Gitlab { .. }));
assert_eq!(a.to_git_url(), "https://gitlab.com/user/repo.git");
// Common typo / habit: trailing .git tolerated.
let b = Address::parse("gitlab:user/repo.git").expect("trailing .git tolerated");
assert_eq!(b.to_git_url(), "https://gitlab.com/user/repo.git");
}
// ---------------------------------------------------------------------------
// Review Second-Pass High #1: update actually replaces a moved install
// ---------------------------------------------------------------------------
/// Publish a new version on an existing bare repo by cloning it,
/// bumping `pmacs.toml`'s `version`, committing, tagging, and pushing
/// back. Returns the commit SHA on the new tag for assertions.
fn publish_new_version(bare: &Path, name: &str, new_version: &str) -> String {
let work = bare.parent().expect("bare has parent").join("work-bump");
run_git(&[OsStr::new("clone"), bare.as_os_str(), work.as_os_str()]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.email"),
OsStr::new("test@example.com"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.name"),
OsStr::new("Tester"),
]);
let manifest = format!(
"name = \"{name}\"\n\
version = \"{new_version}\"\n\
summary = \"M7 review fixture: {name}\"\n\
pmacs_required = \">= 0.1.0\"\n\
entry = \"init.lua\"\n\
exports = [\"{name}\"]\n"
);
std::fs::write(work.join("pmacs.toml"), manifest).expect("rewrite manifest");
std::fs::write(
work.join("init.lua"),
format!("return {{ name = '{name}', version = '{new_version}' }}\n"),
)
.expect("rewrite entry");
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("add"),
OsStr::new("."),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("commit"),
OsStr::new("-m"),
OsStr::new(&format!("v{new_version}")),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("tag"),
OsStr::new(&format!("v{new_version}")),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("push"),
OsStr::new("--tags"),
OsStr::new("origin"),
OsStr::new("main"),
]);
let out = Command::new("git")
.arg("-C")
.arg(&work)
.arg("rev-parse")
.arg("HEAD")
.output()
.expect("rev-parse HEAD");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn update_replaces_install_when_upstream_publishes_new_version() {
let (_td, bare) = make_pkg_with_dep("review-bump", None, "");
let addr = file_url(&bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
// Install at ^1.0.0 --- picks v1.0.0 (the only available tag).
let install_script =
format!(r#"pmacs.packages.install {{ "git:{addr}", version = "^1.0.0" }}"#);
host.eval(Some("install_v1"), &install_script)
.unwrap_or_else(|e| panic!("baseline install failed: {e}"));
let install_dir = user_root.path().join("review-bump");
let entry_v1 =
std::fs::read_to_string(install_dir.join("init.lua")).expect("read entry after v1 install");
assert!(
entry_v1.contains("'1.0.0'"),
"v1 install should ship version 1.0.0 entry; got {entry_v1}"
);
// Upstream publishes 1.1.0.
let v1_1_commit = publish_new_version(&bare, "review-bump", "1.1.0");
// update() should re-resolve and replace the on-disk install.
let update_script = r#"
local summary = pmacs.packages.update()
for _, row in ipairs(summary) do
if row.name == "review-bump" then
assert(row.version == "1.1.0",
"update should land version 1.1.0; got " .. tostring(row.version))
assert(row.changed == true,
"update should report changed = true; got " .. tostring(row.changed))
return
end
end
error("review-bump missing from update summary")
"#;
host.eval(Some("update_to_v1_1"), update_script)
.unwrap_or_else(|e| panic!("update failed: {e}"));
let entry_v11 = std::fs::read_to_string(install_dir.join("init.lua"))
.expect("read entry after v1.1 update");
assert!(
entry_v11.contains("'1.1.0'"),
"post-update install should ship version 1.1.0 entry; got {entry_v11}"
);
// Lockfile reflects the new commit.
let lock = pmacs::packages::Lockfile::read_from(&user_root.path().join("pmacs.lock"))
.expect("parse lockfile");
let entry = lock
.packages
.iter()
.find(|e| e.name.as_str() == "review-bump")
.expect("review-bump in lockfile");
assert_eq!(
entry.commit, v1_1_commit,
"lockfile commit should match newly-published v1.1.0 HEAD"
);
// No leftover staging dir.
let staged = user_root.path().join("review-bump.new");
assert!(
!staged.exists(),
"no leftover staging dir after successful update"
);
}
// ---------------------------------------------------------------------------
// Review Second-Pass High #2: top-level install honors resolver's commit
// ---------------------------------------------------------------------------
#[test]
fn top_level_install_uses_resolver_commit_not_independent_tag_pick() {
// Setup: app depends on dep ^1.0.0. Both publish v1.0.0 first;
// then dep publishes v1.1.0 with a much higher pmacs_required
// that's incompatible with the running build. The resolver must
// pick dep v1.0.0 to satisfy compatibility; the installer must
// not independently re-pick v1.1.0 for dep when invoked through
// the user-facing install.
//
// Setup pivot: this test isn't about a *transitive* dep but
// about the *top-level* package's tag pick. So we structure it
// as: app v1.0.0 (compat), then publish app v1.1.0 with a
// pmacs_required that won't match the running version. Install
// with `version = "^1.0.0"` --- resolver picks v1.0.0 (v1.1.0
// ineligible due to pmacs_required), installer must follow.
let (_td, bare) = make_pkg_with_dep("review-resolver-pick", None, "");
let addr = file_url(&bare);
// Publish v1.1.0 with an unrealistically-high pmacs_required.
let work = bare
.parent()
.expect("bare has parent")
.join("work-incompat");
run_git(&[OsStr::new("clone"), bare.as_os_str(), work.as_os_str()]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.email"),
OsStr::new("test@example.com"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.name"),
OsStr::new("Tester"),
]);
let bad_manifest = "name = \"review-resolver-pick\"\n\
version = \"1.1.0\"\n\
summary = \"resolver-incompat fixture\"\n\
pmacs_required = \">= 99.0.0\"\n\
entry = \"init.lua\"\n\
exports = [\"review-resolver-pick\"]\n";
std::fs::write(work.join("pmacs.toml"), bad_manifest).expect("rewrite manifest");
std::fs::write(
work.join("init.lua"),
"return { name = 'review-resolver-pick', version = '1.1.0' }\n",
)
.expect("rewrite entry");
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("add"),
OsStr::new("."),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("commit"),
OsStr::new("-m"),
OsStr::new("v1.1.0"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("tag"),
OsStr::new("v1.1.0"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("push"),
OsStr::new("--tags"),
OsStr::new("origin"),
OsStr::new("main"),
]);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
// Constraint ^1.0.0 admits both v1.0.0 and v1.1.0 by version,
// but v1.1.0's pmacs_required (>= 99.0.0) is incompatible. The
// resolver must prefer v1.0.0; if the installer independently
// re-picks v1.1.0 it errors with PmacsVersionIncompatible
// instead of installing successfully.
let script = format!(
r#"
local pkg = pmacs.packages.install {{ "git:{addr}", version = "^1.0.0" }}
assert(pkg.version == "1.0.0",
"expected resolver to pick v1.0.0, installer to honor it; got " .. tostring(pkg.version))
"#
);
host.eval(Some("resolver_pick"), &script)
.unwrap_or_else(|e| {
panic!("install must succeed at v1.0.0 via resolver pick (not v1.1.0): {e}")
});
}
// ---------------------------------------------------------------------------
// Review Third-Pass Medium #1: update prunes dropped transitive deps
// ---------------------------------------------------------------------------
/// Publish a new version on `bare` with an optional `[[dependencies]]`
/// block. When `dep_addr` is None, the new revision drops every
/// dependency it had before --- the case the prune test exercises.
fn publish_new_version_with_optional_dep(
bare: &Path,
name: &str,
new_version: &str,
dep_addr: Option<&str>,
dep_constraint: &str,
) -> String {
let work = bare
.parent()
.expect("bare has parent")
.join(format!("work-bump-{new_version}"));
run_git(&[OsStr::new("clone"), bare.as_os_str(), work.as_os_str()]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.email"),
OsStr::new("test@example.com"),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("config"),
OsStr::new("user.name"),
OsStr::new("Tester"),
]);
let mut manifest = format!(
"name = \"{name}\"\n\
version = \"{new_version}\"\n\
summary = \"M7 review fixture: {name}\"\n\
pmacs_required = \">= 0.1.0\"\n\
entry = \"init.lua\"\n\
exports = [\"{name}\"]\n"
);
if let Some(addr) = dep_addr {
use std::fmt::Write;
write!(
manifest,
"\n[[dependencies]]\naddress = \"git:{addr}\"\nversion = \"{dep_constraint}\"\n"
)
.expect("write into String never fails");
}
std::fs::write(work.join("pmacs.toml"), manifest).expect("rewrite manifest");
std::fs::write(
work.join("init.lua"),
format!("return {{ name = '{name}', version = '{new_version}' }}\n"),
)
.expect("rewrite entry");
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("add"),
OsStr::new("."),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("commit"),
OsStr::new("-m"),
OsStr::new(&format!("v{new_version}")),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("tag"),
OsStr::new(&format!("v{new_version}")),
]);
run_git(&[
OsStr::new("-C"),
work.as_os_str(),
OsStr::new("push"),
OsStr::new("--tags"),
OsStr::new("origin"),
OsStr::new("main"),
]);
let out = Command::new("git")
.arg("-C")
.arg(&work)
.arg("rev-parse")
.arg("HEAD")
.output()
.expect("rev-parse HEAD");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn update_prunes_packages_dropped_from_new_resolve_plan() {
// App v1.0.0 depends on dep ^1.0.0; install pulls both. App
// v1.1.0 drops the dep entirely. After update(): the dep must
// disappear from the lockfile, the on-disk install dir, the
// installed() roster, and require() reachability.
let (_dep_td, dep_bare) = make_pkg_with_dep("review-prune-dep", None, "");
let dep_addr = file_url(&dep_bare);
let (_app_td, app_bare) = make_pkg_with_dep("review-prune-app", Some(&dep_addr), "^1.0.0");
let app_addr = file_url(&app_bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
// Install pulls in both packages.
let install_script =
format!(r#"pmacs.packages.install {{ "git:{app_addr}", version = "^1.0.0" }}"#);
host.eval(Some("install_with_dep"), &install_script)
.unwrap_or_else(|e| panic!("install with dep failed: {e}"));
let dep_dir = user_root.path().join("review-prune-dep");
let app_dir = user_root.path().join("review-prune-app");
assert!(dep_dir.exists(), "dep dir must exist after initial install");
assert!(app_dir.exists(), "app dir must exist after initial install");
// Upstream publishes app v1.1.0 with no dependencies.
publish_new_version_with_optional_dep(&app_bare, "review-prune-app", "1.1.0", None, "");
// update() should drop the dep.
let update_script = r#"
local summary = pmacs.packages.update()
local names = {}
for _, row in ipairs(summary) do names[row.name] = row end
assert(names["review-prune-app"], "app must remain in summary")
assert(not names["review-prune-dep"],
"dropped dep must NOT appear in update summary")
-- Roster reflects the prune.
local roster = pmacs.packages.installed()
for _, p in ipairs(roster) do
assert(p.name ~= "review-prune-dep",
"review-prune-dep must be gone from installed() roster, got " .. p.name)
end
-- require() of the dropped dep must fail.
local ok = pcall(require, "review-prune-dep")
assert(not ok,
"require('review-prune-dep') must fail after prune; it succeeded")
"#;
host.eval(Some("update_drops_dep"), update_script)
.unwrap_or_else(|e| panic!("update prune failed: {e}"));
// On-disk: dep dir gone, app dir still there.
assert!(
!dep_dir.exists(),
"dep install dir must be removed after prune"
);
assert!(app_dir.exists(), "app install dir must persist");
// Lockfile reflects the prune.
let lock = pmacs::packages::Lockfile::read_from(&user_root.path().join("pmacs.lock"))
.expect("parse lockfile");
let names: Vec<&str> = lock.packages.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"review-prune-app"),
"lockfile must keep app: {names:?}"
);
assert!(
!names.contains(&"review-prune-dep"),
"lockfile must drop pruned dep: {names:?}"
);
}
// ---------------------------------------------------------------------------
// Review Fourth-Pass: package.loaded invalidated after update
// ---------------------------------------------------------------------------
/// A `require()` against a package the update prunes must fail
/// even when the dep was already cached in `package.loaded` at
/// update time. Without invalidation, Lua would happily return the
/// stale cached module table and the user wouldn't notice the
/// prune until the next process restart.
#[test]
fn require_after_update_prune_returns_stale_then_fails_after_invalidate() {
let (_dep_td, dep_bare) = make_pkg_with_dep("review-cache-dep", None, "");
let dep_addr = file_url(&dep_bare);
let (_app_td, app_bare) = make_pkg_with_dep("review-cache-app", Some(&dep_addr), "^1.0.0");
let app_addr = file_url(&app_bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
// Install + require + cache-warm: package.loaded["review-cache-dep"]
// is now populated with the dep's module table.
let warm = format!(
r#"
pmacs.packages.install {{ "git:{app_addr}", version = "^1.0.0" }}
local d = require("review-cache-dep")
assert(d.name == "review-cache-dep")
local a = require("review-cache-app")
assert(a.name == "review-cache-app")
-- Prove the dep is in the cache, not just findable on disk.
assert(package.loaded["review-cache-dep"] ~= nil,
"package.loaded must hold the dep before update")
"#
);
host.eval(Some("warm"), &warm)
.unwrap_or_else(|e| panic!("warm-up failed: {e}"));
// Upstream drops the dep.
publish_new_version_with_optional_dep(&app_bare, "review-cache-app", "1.1.0", None, "");
let after = r#"
pmacs.packages.update()
-- Must be invalidated from the cache and re-require must fail.
assert(package.loaded["review-cache-dep"] == nil,
"update must clear package.loaded[dep]")
local ok, err = pcall(require, "review-cache-dep")
assert(not ok,
"require('review-cache-dep') must fail after prune+invalidate; succeeded with " ..
tostring(err))
"#;
host.eval(Some("after"), after)
.unwrap_or_else(|e| panic!("post-update require check failed: {e}"));
}
/// `require()` after an update that moved a package's commit must
/// return the *new* version's module table, not the cached old
/// one. Exercises the same invalidation as the prune test but for
/// the version-bump path.
#[test]
fn require_after_update_returns_new_version_not_cached_old() {
let (_td, bare) = make_pkg_with_dep("review-cache-bump", None, "");
let addr = file_url(&bare);
let cache = tempfile::tempdir().expect("cache");
let user_root = tempfile::tempdir().expect("user-root");
let mut host = LuaHost::new().expect("LuaHost::new");
host.set_package_install_override(
PackageInstallOverride::new()
.with_cache_dir(cache.path().to_path_buf())
.with_user_install_root(user_root.path().to_path_buf()),
);
// Install v1.0.0, require, observe the version field.
let warm = format!(
r#"
pmacs.packages.install {{ "git:{addr}", version = "^1.0.0" }}
local m = require("review-cache-bump")
assert(m.version == "1.0.0",
"warm-up: expected v1.0.0, got " .. tostring(m.version))
"#
);
host.eval(Some("warm_v1"), &warm)
.unwrap_or_else(|e| panic!("warm-up failed: {e}"));
// Publish v1.1.0 upstream.
publish_new_version_with_optional_dep(&bare, "review-cache-bump", "1.1.0", None, "");
let after = r#"
pmacs.packages.update()
-- The cached module from v1.0.0 must have been dropped, so
-- this require() re-runs the chunk and returns v1.1.0's
-- module table.
local m = require("review-cache-bump")
assert(m.version == "1.1.0",
"post-update require should return v1.1.0, got " .. tostring(m.version))
"#;
host.eval(Some("after_v1_1"), after)
.unwrap_or_else(|e| panic!("post-update require check failed: {e}"));
}