pmacs/tests/m8_1c_acceptance.rs

249 lines
9.7 KiB
Rust

// tests/m8_1c_acceptance.rs --- T M8.1c install_local acceptance.
//! Acceptance tests for `pmacs.packages.install_local(path)`.
//!
//! `install_local` is the dev-loop counterpart to
//! `pmacs.packages.install`: it symlinks a working tree into the
//! install root rather than fetching + extracting an archive.
//! Edits to the source survive without re-running the install path,
//! and `pmacs.packages.reload(name)` (M8.1d) picks them up without
//! restarting the editor.
//!
//! Tests pin the contracts the package author guide will document:
//!
//! - The install path is a symlink, not a real directory.
//! - The roster entry's `pin` reports `kind = "local"` and
//! `value = <canonical source path>`.
//! - `require("<pkg>")` resolves against the symlinked source, so
//! the package's `init.lua` runs.
//! - Calling `install_local` twice for the same name swaps the
//! symlink (and invalidates `package.loaded` so a re-`require`
//! against the new target works).
//! - `install_local` refuses to overwrite a real (fetched) install
//! at the same name.
//! - No lockfile entry is written.
use std::path::PathBuf;
use pmacs::lua::LuaHost;
use pmacs::lua_bindings::PackageInstallOverride;
use tempfile::TempDir;
/// Build a working-tree directory containing a valid pmacs.toml +
/// init.lua. Returns the directory's tempdir guard and the path.
/// The init.lua returns a small module table so a require can
/// observe it.
fn make_local_pkg(name: &str, version_marker: &str) -> (TempDir, PathBuf) {
let td = tempfile::tempdir().expect("tempdir");
let dir = td.path().to_path_buf();
let manifest = format!(
"name = \"{name}\"\n\
version = \"1.0.0\"\n\
summary = \"M8.1c install_local fixture: {name}\"\n\
pmacs_required = \">= 0.1.0\"\n\
entry = \"init.lua\"\n\
exports = [\"{name}\"]\n"
);
std::fs::write(dir.join("pmacs.toml"), manifest).expect("write manifest");
std::fs::write(
dir.join("init.lua"),
format!("return {{ name = '{name}', marker = '{version_marker}' }}\n"),
)
.expect("write entry");
(td, dir)
}
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)
}
// ---------------------------------------------------------------------------
// T M8.1c --- install_local creates a symlink and the package is requireable
// ---------------------------------------------------------------------------
#[test]
fn install_local_creates_a_symlink_and_require_works() {
let (_pkg_td, pkg_path) = make_local_pkg("local-alpha", "v1");
let pkg_str = pkg_path.display().to_string();
let (mut host, _c, user_root) = host_with_overrides();
let script = format!(
r#"
local pkg = pmacs.packages.install_local("{pkg_str}")
assert(pkg.name == "local-alpha", "pkg.name should be 'local-alpha', got " .. tostring(pkg.name))
assert(pkg.pin.kind == "local",
"pin.kind should be 'local', got " .. tostring(pkg.pin.kind))
-- Require the package; chunk runs from the symlinked tree.
local m = require("local-alpha")
assert(m.name == "local-alpha")
assert(m.marker == "v1")
return "ok"
"#
);
host.eval(Some("install_local"), &script)
.unwrap_or_else(|e| panic!("install_local + require failed: {e}"));
// The install path must be a symlink (not a real dir).
let install_path = user_root.path().join("local-alpha");
let meta = std::fs::symlink_metadata(&install_path).expect("symlink_metadata");
assert!(
meta.file_type().is_symlink(),
"install path must be a symlink, got file_type {:?}",
meta.file_type()
);
let target = std::fs::read_link(&install_path).expect("read_link");
assert_eq!(
target,
std::fs::canonicalize(&pkg_path).unwrap(),
"symlink target must be the canonicalized source path"
);
}
// ---------------------------------------------------------------------------
// T M8.1c --- install_local does not write a lockfile entry
// ---------------------------------------------------------------------------
#[test]
fn install_local_skips_lockfile_entry() {
let (_pkg_td, pkg_path) = make_local_pkg("local-no-lock", "v1");
let pkg_str = pkg_path.display().to_string();
let (mut host, _c, user_root) = host_with_overrides();
let script = format!(r#"pmacs.packages.install_local("{pkg_str}")"#);
host.eval(Some("install"), &script)
.unwrap_or_else(|e| panic!("install_local failed: {e}"));
// No `pmacs.lock` file should exist after an install_local-only run.
let lock_path = user_root.path().join("pmacs.lock");
assert!(
!lock_path.exists(),
"install_local must not write a lockfile; found one at {lock_path:?}"
);
}
// ---------------------------------------------------------------------------
// T M8.1c --- install_local replaces an existing symlink (different source)
// ---------------------------------------------------------------------------
#[test]
fn install_local_replaces_existing_symlink_to_a_different_source() {
let (_a_td, a_path) = make_local_pkg("local-swap", "from-a");
let (_b_td, b_path) = make_local_pkg("local-swap", "from-b");
let a_str = a_path.display().to_string();
let b_str = b_path.display().to_string();
let (mut host, _c, user_root) = host_with_overrides();
let script = format!(
r#"
pmacs.packages.install_local("{a_str}")
local before = require("local-swap")
assert(before.marker == "from-a", "first install should yield 'from-a'")
pmacs.packages.install_local("{b_str}")
-- install_local invalidates package.loaded so re-require
-- picks up the new symlink target's chunk.
local after = require("local-swap")
assert(after.marker == "from-b",
"after second install_local, require should return 'from-b', got " .. tostring(after.marker))
"#
);
host.eval(Some("swap"), &script)
.unwrap_or_else(|e| panic!("install_local swap failed: {e}"));
let target =
std::fs::read_link(user_root.path().join("local-swap")).expect("read_link after swap");
assert_eq!(
target,
std::fs::canonicalize(&b_path).unwrap(),
"symlink must point at the second install_local source after swap"
);
}
// ---------------------------------------------------------------------------
// T M8.1c --- install_local refuses a path with no pmacs.toml
// ---------------------------------------------------------------------------
#[test]
fn install_local_on_path_without_manifest_fails_with_clear_error() {
let bad = tempfile::tempdir().expect("tempdir");
// Just a directory with no manifest.
std::fs::write(bad.path().join("readme.md"), b"hi").expect("write");
let bad_str = bad.path().display().to_string();
let (mut host, _c, _u) = host_with_overrides();
let script = format!(r#"pmacs.packages.install_local("{bad_str}")"#);
let err = host
.eval(Some("missing"), &script)
.expect_err("must fail without manifest");
let msg = err.to_string();
assert!(
msg.contains("pmacs.toml"),
"error must mention pmacs.toml; got {msg}"
);
assert!(
msg.contains(&bad_str) || msg.contains("install_local"),
"error must name the path or the call; got {msg}"
);
}
// ---------------------------------------------------------------------------
// T M8.1c --- install_local refuses to overwrite a real install
// ---------------------------------------------------------------------------
#[test]
fn install_local_refuses_to_overwrite_a_real_install_dir() {
let (_pkg_td, pkg_path) = make_local_pkg("local-collide", "v1");
let pkg_str = pkg_path.display().to_string();
let (mut host, _c, user_root) = host_with_overrides();
// Plant a real (non-symlink) directory at the install path that
// install_local would otherwise occupy. install_local must
// refuse rather than blow it away --- a fetched install with
// user-modified files would otherwise be silently lost.
let collide_path = user_root.path().join("local-collide");
std::fs::create_dir_all(&collide_path).expect("plant real dir");
std::fs::write(collide_path.join("important.lua"), b"-- do not delete\n")
.expect("write important file");
let script = format!(r#"pmacs.packages.install_local("{pkg_str}")"#);
let err = host
.eval(Some("collide"), &script)
.expect_err("must refuse real-install collision");
let msg = err.to_string();
assert!(
msg.contains("real install") || msg.contains("not a symlink"),
"error must explain why install_local refused; got {msg}"
);
// The original directory survives.
assert!(
collide_path.join("important.lua").exists(),
"install_local must not have touched the real install"
);
}
// ---------------------------------------------------------------------------
// T M8.1c --- LockfilePin::from_install_pin returns None for Local
// ---------------------------------------------------------------------------
#[test]
fn lockfile_pin_from_install_pin_returns_none_for_local() {
use pmacs::packages::{InstallPin, LockfilePin};
let pin = InstallPin::Local {
source_path: PathBuf::from("/tmp/wherever"),
};
assert!(
LockfilePin::from_install_pin(&pin).is_none(),
"Local pins must not produce a LockfilePin"
);
}