// tests/m7_7_acceptance.rs --- Acceptance tests for T M7.7 (loader / require). //! End-to-end acceptance for T M7.7 (loader and `require` integration). //! Spec acceptance bullets (`pmacs-tasks.tex:3300-3311`): //! //! 1. `require("pmacs-magit")` (or whatever a package's entry is) //! loads its entry module successfully. //! 2. Attempting to `require("pmacs-magit.internal")` (non-exported) //! fails with a clear message naming the package and the missing //! export. //! 3. Lua tests verify the environment-table boundary: package A //! cannot accidentally pollute package B's globals. //! 4. `describe-package` (here: `pmacs.packages.describe`) reports a //! package's metadata, version, and exports list. use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::process::Command; use pmacs::lua::LuaHost; use pmacs::lua_bindings::PackageInstallOverride; use tempfile::TempDir; // --------------------------------------------------------------------------- // Fixture: bare repo with custom layout // --------------------------------------------------------------------------- /// Build a bare repo with a manifest that declares the given exports /// list, plus a set of (relative-path → file-body) extra files in the /// package tree. The package always has an `init.lua` entry returning /// `{ name = name, version = "1.0.0", ... }` so the bullet-1 path is /// covered without bespoke fixtures. fn make_pkg_with_exports( name: &str, exports: &[&str], extra_files: &[(&str, &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 exports_lua = exports .iter() .map(|e| format!("\"{e}\"")) .collect::>() .join(", "); let manifest = format!( "name = \"{name}\"\n\ version = \"1.0.0\"\n\ summary = \"acceptance fixture\"\n\ pmacs_required = \">= 0.1.0\"\n\ entry = \"init.lua\"\n\ exports = [{exports_lua}]\n", ); 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"); for (rel, body) in extra_files { let p = work.join(rel); if let Some(parent) = p.parent() { std::fs::create_dir_all(parent).expect("mkdir extra"); } std::fs::write(&p, body).expect("write extra"); } 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 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()) } 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) } // --------------------------------------------------------------------------- // Bullet 1: require entry module // --------------------------------------------------------------------------- #[test] fn require_loads_entry_module_for_installed_package() { let (_td, bare) = make_pkg_with_exports("samplepkg", &["samplepkg"], &[]); let url = file_url(&bare); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local mod = require("samplepkg") assert(mod.name == "samplepkg", "wrong name: " .. tostring(mod.name)) assert(mod.version == "1.0.0", "wrong version: " .. tostring(mod.version)) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("require entry failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } // --------------------------------------------------------------------------- // Bullet 2: non-exported submodule require fails clearly // --------------------------------------------------------------------------- #[test] fn require_non_exported_submodule_fails_with_named_error() { // The package ships an `internal.lua` file but does not list it in // `exports`. `require("samplepkg.internal")` must fail with a // message naming the package and the missing export. let (_td, bare) = make_pkg_with_exports( "samplepkg", &["samplepkg.public"], &[ ("public.lua", "return { kind = 'public' }\n"), ("internal.lua", "return { kind = 'internal' }\n"), ], ); let url = file_url(&bare); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local ok, err = pcall(require, "samplepkg.internal") assert(not ok, "require of unexported submodule unexpectedly succeeded") local msg = tostring(err) assert(string.find(msg, "samplepkg", 1, true), "error must name package: " .. msg) assert(string.find(msg, "internal", 1, true), "error must name missing export: " .. msg) assert(string.find(msg, "samplepkg.public", 1, true), "error must list available exports: " .. msg) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("non-exported require check failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } #[test] fn require_exported_submodule_loads_module() { let (_td, bare) = make_pkg_with_exports( "samplepkg", &["samplepkg.public"], &[("public.lua", "return { kind = 'public' }\n")], ); let url = file_url(&bare); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local mod = require("samplepkg.public") assert(mod.kind == "public", "wrong kind: " .. tostring(mod.kind)) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("exported submodule require failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } // --------------------------------------------------------------------------- // Bullet 3: per-package environment isolation // --------------------------------------------------------------------------- #[test] fn package_global_writes_stay_local_to_package_env() { // Two packages, each writes a same-name "global" in their entry // chunks. Neither write should touch the real `_G`, and each // package's entry table must reflect its own write only. // // The fixture entries are crafted to: // - Set a global `MARKER` to a per-package string. // - Read `MARKER` back and return both `MARKER` and a // stdlib-derived value (`_G.tostring(...)`) — the read goes // through __index on the env's metatable, so stdlib stays // reachable even though the package has its own `_ENV`. // - Return the package's own `_PACKAGE.name` for introspection. let (_td_a, bare_a) = make_pkg_with_exports("pkg-a", &["pkg-a"], &[]); let (_td_b, bare_b) = make_pkg_with_exports("pkg-b", &["pkg-b"], &[]); // Override entry bodies post-fixture by re-cloning, mutating, and // re-tagging would be invasive. Simpler: ship the test's expectations // through a single-package fixture that we instrument inline. let _ = (bare_a, bare_b); // (unused — using inline fixture below) // Two packages with custom entry bodies that touch globals. let (_td_a, bare_a) = make_pkg_with_custom_entry( "pkg-a", "MARKER = 'from-a'\nreturn { marker = MARKER, pkg = _PACKAGE.name }\n", ); let (_td_b, bare_b) = make_pkg_with_custom_entry( "pkg-b", "MARKER = 'from-b'\nreturn { marker = MARKER, pkg = _PACKAGE.name }\n", ); let url_a = file_url(&bare_a); let url_b = file_url(&bare_b); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url_a}", version = "^1.0.0" }} pmacs.packages.install {{ "git:{url_b}", version = "^1.0.0" }} local a = require("pkg-a") local b = require("pkg-b") assert(a.marker == "from-a", "pkg-a saw wrong marker: " .. tostring(a.marker)) assert(b.marker == "from-b", "pkg-b saw wrong marker: " .. tostring(b.marker)) assert(a.pkg == "pkg-a", "pkg-a _PACKAGE.name wrong: " .. tostring(a.pkg)) assert(b.pkg == "pkg-b", "pkg-b _PACKAGE.name wrong: " .. tostring(b.pkg)) -- The real _G must not have been polluted by either package. assert(MARKER == nil, "_G.MARKER should be nil after package loads, got: " .. tostring(MARKER)) assert(rawget(_G, "MARKER") == nil, "rawget(_G, 'MARKER') should be nil, got: " .. tostring(rawget(_G, "MARKER"))) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("env isolation failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } /// Build a bare repo with a custom entry body. Variant of /// `make_pkg_with_exports` that lets tests inject globals-touching /// chunks into the package's `init.lua`. fn make_pkg_with_custom_entry(name: &str, init_body: &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 manifest = format!( "name = \"{name}\"\n\ version = \"1.0.0\"\n\ summary = \"acceptance fixture\"\n\ pmacs_required = \">= 0.1.0\"\n\ entry = \"init.lua\"\n\ exports = [\"{name}\"]\n", ); std::fs::write(work.join("pmacs.toml"), manifest).expect("write manifest"); std::fs::write(work.join("init.lua"), init_body).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) } #[test] fn package_can_read_stdlib_through_env_metatable() { // The per-package env's __index points at _G, so packages can // call stdlib functions transparently. A package that uses // `string.format`, `tostring`, and `pmacs.` (if available) // through its env should work without explicit imports. let (_td, bare) = make_pkg_with_custom_entry( "stdlib-user", "local out = string.format('rev=%s', tostring(42))\n\ return { out = out }\n", ); let url = file_url(&bare); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local mod = require("stdlib-user") assert(mod.out == "rev=42", "stdlib not reachable: " .. tostring(mod.out)) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("stdlib via env failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } // --------------------------------------------------------------------------- // Bullet 4: describe-package reports metadata // --------------------------------------------------------------------------- #[test] fn describe_returns_full_metadata_for_installed_package() { let (_td, bare) = make_pkg_with_exports( "samplepkg", &["samplepkg", "samplepkg.public"], &[("public.lua", "return {}\n")], ); let url = file_url(&bare); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local d = pmacs.packages.describe("samplepkg") assert(d ~= nil, "describe returned nil") assert(d.name == "samplepkg", "wrong name: " .. tostring(d.name)) assert(d.version == "1.0.0", "wrong version: " .. tostring(d.version)) assert(d.summary == "acceptance fixture", "wrong summary: " .. tostring(d.summary)) assert(d.pmacs_required == ">=0.1.0" or d.pmacs_required == ">= 0.1.0", "pmacs_required wrong: " .. tostring(d.pmacs_required)) assert(type(d.exports) == "table", "exports must be a table") assert(#d.exports == 2, "expected 2 exports, got " .. tostring(#d.exports)) -- Exports order mirrors the manifest's declaration order. assert(d.exports[1] == "samplepkg" and d.exports[2] == "samplepkg.public", "unexpected exports: " .. table.concat(d.exports, ",")) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("describe failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } #[test] fn describe_returns_nil_for_unknown_package() { let (mut host, _c, _u) = host_with_overrides(); host.eval( Some("test"), r#" local d = pmacs.packages.describe("not-installed") assert(d == nil, "expected nil, got " .. type(d)) "#, ) .unwrap_or_else(|e| panic!("describe-nil failed: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); } // --------------------------------------------------------------------------- // Searcher precedence: pmacs roster wins over package.path // --------------------------------------------------------------------------- #[test] fn pmacs_searcher_runs_before_package_path() { // If a non-installed name is on package.path AND a similarly named // pmacs package is installed, the searcher must route through the // installed package (not the path) — exports gating depends on it. // // We construct: a package "shadow" installed via pmacs.packages.install, // PLUS a regular Lua file at path/to/shadow.lua placed on package.path. // require("shadow") must return the package's entry table (with // marker = "from-pmacs"), not the path file's table. let (_td, bare) = make_pkg_with_custom_entry("shadow", "return { marker = 'from-pmacs' }\n"); let url = file_url(&bare); // Stage a path-side shadow.lua. let path_dir = tempfile::tempdir().expect("path dir"); std::fs::write( path_dir.path().join("shadow.lua"), "return { marker = 'from-path' }\n", ) .expect("write path shadow"); let path_dir_str = path_dir.path().display().to_string(); let (mut host, _c, _u) = host_with_overrides(); let script = format!( r#" package.path = "{path_dir_str}/?.lua;" .. package.path pmacs.packages.install {{ "git:{url}", version = "^1.0.0" }} local mod = require("shadow") assert(mod.marker == "from-pmacs", "pmacs roster should win over package.path; got " .. tostring(mod.marker)) "#, ); host.eval(Some("test"), &script) .unwrap_or_else(|e| panic!("searcher precedence: {e}")); assert!(host.errors().is_empty(), "errors: {:?}", host.errors()); }