test(isolation): census, hostile-environment proof, adoption ratchet

The census first, because it decides how large the mechanical edit is
(framing §7). Every occurrence was listed with its enclosing context and
read; a grep for the bare name over-counts, which is how revision 1
reported 18 by grepping `Editor::new` — a pattern that does not match the
real constructor.

  in-process   342 calls in 66 of 97 files
               (330 of 334 `EditorState::new()` occurrences; 4 are prose)
               (12 of 14 `EditorState::open(` occurrences; 2 are strings)
  spawned      14 real `pmacs` spawns in 8 files
               (of 36 `CARGO_BIN_EXE_pmacs` hits, 18 are the fake-LSP and
                fake-MCP siblings and 4 are path derivations for
                `pmacs-gpu`, not spawns)
  mixed        5 files are both, so sites — not files — are the unit

The full census, with per-site attribution, is the module doc of
`tests/ambient_isolation_acceptance.rs`.

Four things it pins:

* Isolated construction still finishes initialization, asserted twice —
  the flag, and the behaviour it gates (`pmacs.attach` must refuse).
  Falsified by wrapping the config block in `if roots.is_ambient()`;
  `m8_2_acceptance` does NOT catch that, because reopening an already-open
  init phase is a no-op.
* The writes land in the redirected data root — content produced, not an
  invariant preserved. A "the real root did not change" check would pass
  vacuously wherever it already holds identical bytes, since
  `write_if_changed` is content-gated.
* Bet 3, in two children with opposite jobs. The positive control proves
  the hostile environment IS hostile (an ambient editor loads its
  `init.lua` and writes its data root); without it the isolation half
  asserts nothing. The isolated child then stays green under the same
  environment and leaves its hostile root byte-identical.
* A durable adoption ratchet, not a one-time census: a source scan that
  fails when a new ambient constructor appears outside a named allowlist,
  plus a check that no allowlist entry has gone dead. Its scanner strips
  comments, strings and raw strings, and that stripping has its own pin —
  the corpus contains all three shapes, and a grep-shaped answer already
  cost this lane a review round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
Levi Neuwirth 2026-07-31 18:48:26 -04:00
parent 85e4ee03bb
commit fcaf0b36fa
3 changed files with 731 additions and 0 deletions

View File

@ -0,0 +1,657 @@
// tests/ambient_isolation_acceptance.rs --- integration tests must not
// read or write the developer's real ambient roots.
//! Acceptance for `docs/test-ambient-config-isolation-framing.md`.
//!
//! # The defect
//!
//! `src/editor.rs` guards user-config loading with `#[cfg(not(test))]`.
//! `cfg(test)` is set only while compiling the crate's *own* unit tests;
//! an integration test in `tests/` links `pmacs` as an ordinary
//! dependency, so the guard is inactive for all of them. `cargo test
//! --lib` is protected, `cargo test --test <name>` is not. And config
//! loading is only the read half: `EditorState::new` materializes
//! bundled packages into `$XDG_DATA_HOME/pmacs` (else
//! `$HOME/.local/share`) **unconditionally**, outside every `cfg` guard,
//! creating directories and writing files.
//!
//! # The census (framing acceptance 1), read at `54a092e`
//!
//! Method: every occurrence was listed with its enclosing context and
//! read. A grep for the bare name over-counts — the framing's revision 1
//! reported 18 by grepping `Editor::new`, which does not even match the
//! real constructor `EditorState::new`.
//!
//! **In-process construction — 342 sites in 66 of 97 files.**
//!
//! * `EditorState::new()` — 334 textual occurrences, of which **330 are
//! calls**. The other 4 are prose: `persistence_acceptance.rs:6` and
//! `:76`, `m7_11_acceptance.rs:92`, `m8_2_acceptance.rs:75`. A fifth
//! file, `m5_6_acceptance.rs:94`, names the constructor only to say it
//! deliberately does **not** use it — the third place in the tree
//! documenting this same `cfg(test)` gap.
//! * `EditorState::open(` — 14 textual occurrences in 3 files, of which
//! **12 are calls** (`journey_acceptance` 7, `m4_acceptance` 4,
//! `theme_faces_acceptance` 1). The other 2 are assertion-message
//! strings in `journey_acceptance.rs:2030,2038`.
//! * Only 329 of the 330 `new()` calls are `let` bindings; the odd one
//! is `m8_1_acceptance.rs:39`, a bare tail expression in a
//! `fresh_editor()` helper. Sites, not files, are the unit.
//!
//! **Spawned `pmacs` — 14 sites in 8 files.** `grep CARGO_BIN_EXE_pmacs`
//! reports 36 sites in 26 files, but 18 of those are the *sibling*
//! binaries `CARGO_BIN_EXE_pmacs_fake_lsp` / `_fake_mcp`, which are LSP
//! and MCP stubs and not pmacs at all. Reading each occurrence:
//!
//! * `tests/common/daemon.rs:158` — the shared `--daemon` harness.
//! * `tests/common/pty.rs:111` — the shared real-PTY spawner.
//! * `m5_7_acceptance.rs` ×5, `m5_8_acceptance.rs` ×3,
//! `m5_5_acceptance.rs` ×1, `m5_perf_acceptance.rs` ×1,
//! `gpu_invocation_acceptance.rs` ×2 — direct `Command::new`.
//! * 4 further occurrences are **path derivations, not spawns**:
//! `gpu_invocation_acceptance.rs:115`, `vterm_stage3_acceptance.rs:644`
//! and `:1180`, `bottom_panel_stage2b_gpu_acceptance.rs:513` all take
//! `CARGO_BIN_EXE_pmacs`'s *parent* to locate the `pmacs-gpu` sibling.
//!
//! **Mixed files are real: 5 files are both in-process and spawned** —
//! `vterm_stage3_acceptance` constructs an editor at `:159` and reaches
//! a daemon at `:665`. A file-level partition cannot represent them,
//! which is why the ratchet below keys on sites.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use pmacs::bootstrap::BootstrapRoots;
use pmacs::editor::EditorState;
#[path = "common/iso.rs"]
mod iso;
// ---------------------------------------------------------------------------
// Isolated construction still finishes initialization (framing §1.8)
// ---------------------------------------------------------------------------
/// **N** — isolated construction skips the ambient *reads* and still
/// flips the init-complete gate.
///
/// Config loading and `set_init_complete()` share one conditional block,
/// so the tempting fix — skip the block when roots are redirected —
/// would leave every isolated test permanently in the init phase.
/// `tests/m8_2_acceptance.rs:75` documents its dependence on
/// integration-test construction being init-complete, so that is not a
/// hypothetical.
///
/// Falsified by wrapping the block in `if roots.is_ambient()`.
#[test]
fn isolated_construction_is_init_complete() {
let state = EditorState::new_with_roots(&iso::roots());
assert!(
state.lua_host.is_init_complete(),
"isolated construction must still leave the init phase, or every \
suite that reopens it (m8_2) breaks"
);
// The paired half: the ambient constructor is unchanged.
let ambient = EditorState::new();
assert!(ambient.lua_host.is_init_complete());
}
/// **N** — the init phase is genuinely closed, not merely reported
/// closed.
///
/// A flag read is one bool; this asserts the *behaviour* the flag gates,
/// so a fix that sets the flag without the surrounding block having run
/// cannot pass. `pmacs.attach` is init-only and must now refuse.
#[test]
fn isolated_construction_closes_the_init_only_lua_surface() {
let state = EditorState::new_with_roots(&iso::roots());
let err = state
.lua_host
.lua()
.load(r#"pmacs.attach { target = "local:/run/pmacs/x.sock" }"#)
.exec()
.expect_err("pmacs.attach must refuse after init");
let text = err.to_string();
assert!(
text.contains("init"),
"the refusal must name the init phase; got {text}"
);
}
// ---------------------------------------------------------------------------
// Isolated construction redirects the writes (framing §1.6)
// ---------------------------------------------------------------------------
/// **N** — the bundled-package materialization lands in the redirected
/// data root.
///
/// Asserts content produced, not an invariant preserved: the package
/// tree has to actually exist under the isolated root. A test that only
/// checked "the real root was not modified" would pass on a machine
/// where the real root already held identical bytes, because
/// `write_if_changed` is content-gated.
#[test]
fn isolated_construction_materializes_into_the_redirected_data_root() {
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("materialize-probe");
let _ = std::fs::remove_dir_all(&base);
let roots = BootstrapRoots::isolated_under(&base);
let dir = roots.bundled_runtime_dir().expect("redirected data root");
assert!(!dir.exists(), "the probe root must start absent");
let _state = EditorState::new_with_roots(&roots);
let manifest = dir.join("repl").join("pmacs.toml");
assert!(
manifest.is_file(),
"bundled packages must materialize under the redirected data \
root; {} is missing",
manifest.display()
);
let text = std::fs::read_to_string(&manifest).expect("read materialized manifest");
assert!(
text.contains("repl"),
"the materialized manifest must be the bundled package's own; got {text:?}"
);
}
/// **N** — `install_state_dirs` honours the redirected state root.
///
/// It runs *after* the constructor, so a constructor-only parameter
/// would leave it resolving `PMACS_STATE_HOME` / `XDG_STATE_HOME` from
/// the environment and hand an isolated session the developer's real
/// state dir.
#[test]
fn install_state_dirs_honours_the_redirected_state_root() {
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("state-probe");
let roots = BootstrapRoots::isolated_under(&base);
let state = EditorState::new_with_roots(&roots);
state.install_state_dirs();
let dir = state
.lua_host
.lua()
.app_data_ref::<pmacs::lua_bindings::StateDir>()
.expect("install_state_dirs must configure a state dir")
.0
.clone();
assert_eq!(
dir,
roots.state_dir().unwrap(),
"state dir must be redirected"
);
let history = state.core.borrow().minibuffer.history_dir.clone();
assert_eq!(
history,
roots.history_dir(),
"minibuffer history must be redirected too"
);
}
// ---------------------------------------------------------------------------
// Hostile ambient environment (framing Bet 3, acceptance 7)
// ---------------------------------------------------------------------------
/// Names the isolated base for the isolated child; its presence is the
/// signal that this process *is* that child.
const ISOLATED_CHILD_BASE: &str = "PMACS_AMBIENT_ISOLATION_CHILD_BASE";
/// Presence marks the ambient positive-control child.
const AMBIENT_CONTROL_CHILD: &str = "PMACS_AMBIENT_ISOLATION_CONTROL";
/// An `init.lua` that leaves a mark an assertion can see. The real
/// developer `init.lua` that produced this lane broke
/// `compile_mode_acceptance` by redefining a command pmacs already
/// defines; a marker global is the same exposure with a cheaper failure
/// mode, and it discriminates read-vs-not-read directly.
const HOSTILE_INIT_LUA: &str = "_G.HOSTILE_INIT_RAN = true\n";
fn hostile_root(name: &str) -> PathBuf {
let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
let _ = std::fs::remove_dir_all(&root);
let config = root.join("pmacs");
std::fs::create_dir_all(&config).expect("create hostile config dir");
std::fs::write(config.join("init.lua"), HOSTILE_INIT_LUA).expect("write hostile init.lua");
// Pre-seed the data root the ambient resolver would use, so a write
// into it is visible as a *change*, not just as a new tree.
let seeded = root
.join("pmacs")
.join("builtin-packages")
.join(format!("v{}", env!("CARGO_PKG_VERSION")));
std::fs::create_dir_all(&seeded).expect("seed hostile data root");
std::fs::write(seeded.join("SEED"), b"pre-seeded\n").expect("write seed marker");
root
}
/// Flat snapshot of a tree: relative path → contents (directories map to
/// the empty vector). Sorted, so comparison is order-independent.
fn snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let rel = path
.strip_prefix(root)
.expect("entry is under root")
.to_path_buf();
if path.is_dir() {
out.insert(rel, Vec::new());
walk(root, &path, out);
} else {
out.insert(rel, std::fs::read(&path).unwrap_or_default());
}
}
}
let mut out = BTreeMap::new();
walk(root, root, &mut out);
out
}
/// The five storage variables plus `HOME`, all aimed at `root`. `HOME`
/// is included here — and only here — because this is the adversary: a
/// machine that leaves an XDG variable unset falls back to it, and the
/// point of a hostile environment is to leave no path out.
fn hostile_env(root: &Path) -> Vec<(&'static str, PathBuf)> {
vec![
("HOME", root.to_path_buf()),
("XDG_CONFIG_HOME", root.to_path_buf()),
("XDG_DATA_HOME", root.to_path_buf()),
("XDG_STATE_HOME", root.to_path_buf()),
("PMACS_STATE_HOME", root.to_path_buf()),
("XDG_CACHE_HOME", root.to_path_buf()),
]
}
fn run_child(test_name: &str, env: Vec<(&'static str, PathBuf)>) -> (bool, String) {
let exe = std::env::current_exe().expect("current test binary");
let output = Command::new(exe)
.args(["--exact", test_name, "--nocapture", "--test-threads=1"])
.envs(env)
.output()
.unwrap_or_else(|e| panic!("re-exec `{test_name}`: {e}"));
let mut log = String::new();
log.push_str("--- stdout ---\n");
log.push_str(&String::from_utf8_lossy(&output.stdout));
log.push_str("--- stderr ---\n");
log.push_str(&String::from_utf8_lossy(&output.stderr));
assert!(
log.contains("1 passed") || !output.status.success(),
"child `{test_name}` ran no test — the `--exact` filter went stale\n{log}"
);
(output.status.success(), log)
}
/// **Positive control.** Under the hostile environment, the *ambient*
/// constructor really is captured by it.
///
/// Without this the isolation assertion below is unfalsifiable: an
/// `init.lua` that never loads under any circumstances would satisfy
/// "the isolated editor did not load it" while proving nothing.
///
/// Runs only as a re-exec'd child (marker set), so an ordinary suite run
/// does not construct an ambient editor.
#[test]
fn ambient_construction_under_a_hostile_environment_is_captured_by_it() {
if std::env::var_os(AMBIENT_CONTROL_CHILD).is_none() {
return;
}
let state = EditorState::new();
let ran: bool = state
.lua_host
.lua()
.load("return _G.HOSTILE_INIT_RAN == true")
.eval()
.expect("read the hostile marker");
assert!(
ran,
"the ambient constructor must load the hostile init.lua — if it \
does not, the isolation assertion proves nothing"
);
// And the write half: the ambient constructor materializes into the
// hostile data root.
let dir = pmacs::builtin_packages::bundled_runtime_dir();
assert!(
dir.join("repl").join("pmacs.toml").is_file(),
"the ambient constructor must write into the hostile data root; \
{} is empty",
dir.display()
);
}
/// The isolated child: same hostile environment, redirected roots.
#[test]
fn isolated_construction_under_a_hostile_environment_ignores_it() {
let Some(base) = std::env::var_os(ISOLATED_CHILD_BASE) else {
return;
};
let base = PathBuf::from(base);
let roots = BootstrapRoots::isolated_under(&base);
let state = EditorState::new_with_roots(&roots);
let ran: bool = state
.lua_host
.lua()
.load("return _G.HOSTILE_INIT_RAN == true")
.eval()
.expect("read the hostile marker");
assert!(!ran, "the hostile init.lua must not have been loaded");
assert!(
state.lua_host.is_init_complete(),
"and initialization must still have finished"
);
// Content produced, in the right place.
let dir = roots.bundled_runtime_dir().expect("redirected data root");
assert!(
dir.join("repl").join("pmacs.toml").is_file(),
"bundled packages must land under the isolated root; {} is empty",
dir.display()
);
}
/// **N** — the whole of Bet 3: green under a hostile environment, and
/// the hostile root byte-identical afterwards.
///
/// Two children, because the two halves need opposite environments to be
/// meaningful: the positive control must be *captured* by its hostile
/// root (and so modifies it), while the isolated child must leave its
/// own hostile root untouched.
#[test]
fn a_hostile_ambient_environment_is_neither_read_nor_written() {
// Half 1 — the control. Its hostile root is expected to change.
let control = hostile_root("hostile-control");
let before_control = snapshot(&control);
let (ok, log) = run_child(
"ambient_construction_under_a_hostile_environment_is_captured_by_it",
{
let mut env = hostile_env(&control);
env.push((AMBIENT_CONTROL_CHILD, PathBuf::from("1")));
env
},
);
assert!(ok, "the ambient positive control must be captured\n{log}");
let after_control = snapshot(&control);
assert_ne!(
before_control, after_control,
"the control's hostile root must have been written into — if it \
was not, this environment is not hostile and the isolated half \
below asserts nothing"
);
// Half 2 — the isolated child. Its hostile root must be untouched.
let hostile = hostile_root("hostile-isolated");
let isolated_base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("hostile-isolated-roots");
let _ = std::fs::remove_dir_all(&isolated_base);
let before = snapshot(&hostile);
assert!(!before.is_empty(), "the hostile root must not be empty");
let (ok, log) = run_child(
"isolated_construction_under_a_hostile_environment_ignores_it",
{
let mut env = hostile_env(&hostile);
env.push((ISOLATED_CHILD_BASE, isolated_base.clone()));
env
},
);
assert!(ok, "the isolated child must stay green\n{log}");
let after = snapshot(&hostile);
assert_eq!(
before, after,
"the hostile root must be byte-identical afterwards — a green \
suite that still wrote into it has not demonstrated isolation"
);
// And the writes went somewhere: the isolated tree exists.
assert!(
isolated_base.join("data").join("pmacs").is_dir(),
"the isolated data root must have been written instead"
);
}
// ---------------------------------------------------------------------------
// Adoption ratchet (framing acceptance 12)
// ---------------------------------------------------------------------------
/// Files permitted to construct an editor through the **ambient** entry
/// points.
///
/// `journey_acceptance` is ambient on purpose: it is the golden-journey
/// ratchet, and its whole claim is that the production entry point
/// `pmacs FILE` calls has a caller. It is isolated by re-execing itself
/// with controlled roots instead (framing §1.10). This file is ambient
/// only inside the positive control above, which never runs except as a
/// deliberately re-exec'd child.
const AMBIENT_ALLOWLIST: &[&str] = &["journey_acceptance.rs", "ambient_isolation_acceptance.rs"];
/// Strip comments and string-literal *contents* from Rust source, so a
/// scan counts calls rather than mentions.
///
/// Both matter here. `m5_6_acceptance.rs:94` names `EditorState::new` in
/// a comment only to say it deliberately does not call it, and
/// `journey_acceptance.rs:2030` carries it inside an assertion message.
/// Raw strings (`r#"..."#`) are pervasive in this suite, so they are
/// handled rather than hoped about.
fn strip_comments_and_strings(src: &str) -> String {
let b: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
// Line comment.
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '/' {
while i < b.len() && b[i] != '\n' {
i += 1;
}
continue;
}
// Block comment (Rust's nest).
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
let mut depth = 1;
i += 2;
while i < b.len() && depth > 0 {
if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
depth += 1;
i += 2;
} else if b[i] == '*' && i + 1 < b.len() && b[i + 1] == '/' {
depth -= 1;
i += 2;
} else {
i += 1;
}
}
out.push(' ');
continue;
}
// Raw string: r, then any number of #, then ".
if b[i] == 'r' {
let mut j = i + 1;
let mut hashes = 0;
while j < b.len() && b[j] == '#' {
hashes += 1;
j += 1;
}
if j < b.len() && b[j] == '"' {
j += 1;
loop {
if j >= b.len() {
break;
}
if b[j] == '"' {
let mut k = j + 1;
let mut seen = 0;
while k < b.len() && b[k] == '#' && seen < hashes {
seen += 1;
k += 1;
}
if seen == hashes {
j = k;
break;
}
}
j += 1;
}
out.push(' ');
i = j;
continue;
}
}
// Ordinary string.
if b[i] == '"' {
i += 1;
while i < b.len() {
if b[i] == '\\' {
i += 2;
continue;
}
if b[i] == '"' {
i += 1;
break;
}
i += 1;
}
out.push(' ');
continue;
}
out.push(b[i]);
i += 1;
}
out
}
fn tests_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}
/// Every `.rs` file under `tests/`, including `tests/common/`.
fn test_sources() -> Vec<(String, String)> {
let mut out = Vec::new();
let dir = tests_dir();
let push_dir = |d: &Path, out: &mut Vec<(String, String)>| {
for entry in std::fs::read_dir(d).expect("read tests dir").flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
let name = path
.file_name()
.expect("file name")
.to_string_lossy()
.into_owned();
out.push((name, std::fs::read_to_string(&path).expect("read source")));
}
}
};
push_dir(&dir, &mut out);
push_dir(&dir.join("common"), &mut out);
out
}
/// **N** — a *durable* adoption ratchet, not a one-time census.
///
/// One self-spawning hostile-environment test proves the seam works; it
/// cannot notice a raw `EditorState::new()` added to a different binary
/// next month. This can. Falsified by adding an ambient constructor to
/// any non-allowlisted suite.
#[test]
fn no_test_outside_the_allowlist_constructs_an_ambient_editor() {
let sources = test_sources();
// A broken glob must not read as a clean tree.
assert!(
sources.len() > 90,
"expected the whole tests/ corpus; found only {} files",
sources.len()
);
let needles = [
concat!("EditorState::", "new()"),
concat!("EditorState::", "open("),
];
let mut offenders: Vec<String> = Vec::new();
let mut seen_allowlisted: Vec<&str> = Vec::new();
for (name, src) in &sources {
let code = strip_comments_and_strings(src);
let hits: usize = needles.iter().map(|n| code.matches(n).count()).sum();
if hits == 0 {
continue;
}
if AMBIENT_ALLOWLIST.contains(&name.as_str()) {
seen_allowlisted.push(
AMBIENT_ALLOWLIST
.iter()
.find(|a| **a == name.as_str())
.expect("just matched"),
);
} else {
offenders.push(format!("{name} ({hits} site(s))"));
}
}
offenders.sort();
assert!(
offenders.is_empty(),
"these suites construct an editor through the ambient entry \
points, so they read the developer's real init.lua and write \
into their real data root: {offenders:?}\n\
Use `EditorState::new_with_roots(&crate::iso::roots())` (see \
tests/common/iso.rs), or add the file to AMBIENT_ALLOWLIST with \
a reason.",
);
// Dead allowlist entries are how a ratchet rots: an entry that no
// longer needs to be there silently licenses a future regression.
let mut missing: Vec<&&str> = AMBIENT_ALLOWLIST
.iter()
.filter(|a| !seen_allowlisted.contains(&**a))
.collect();
missing.sort_unstable();
assert!(
missing.is_empty(),
"allowlisted files that no longer construct an ambient editor — \
remove them: {missing:?}"
);
}
/// **N** — the ratchet's scanner is not fooled by prose.
///
/// A grep-shaped answer is what cost this lane a review round; the scan
/// above only ratchets if it distinguishes a call from a mention. Pinned
/// with the exact shapes the corpus actually contains.
#[test]
fn the_ratchet_scanner_counts_calls_not_mentions() {
let sample = r##"
//! `EditorState::new()` in a doc comment.
// `EditorState::new()` in a line comment.
/* `EditorState::new()` in a block comment. */
fn f() {
let msg = "EditorState::open(file) must not greet";
let raw = r#"EditorState::new() inside a raw string"#;
let _ = EditorState::new();
}
"##;
let code = strip_comments_and_strings(sample);
assert_eq!(
code.matches(concat!("EditorState::", "new()")).count(),
1,
"only the call survives; got {code:?}"
);
assert_eq!(
code.matches(concat!("EditorState::", "open(")).count(),
0,
"the assertion-message mention must not count; got {code:?}"
);
}
/// **N** — the seam actually reached the corpus.
///
/// The ratchet above is an absence check, and an absence check passes on
/// a tree where nobody constructs an editor at all. This asserts the
/// positive: the isolated constructor has broad adoption.
#[test]
fn the_isolated_constructor_has_been_adopted_across_the_corpus() {
let sources = test_sources();
let adopters: Vec<&String> = sources
.iter()
.filter(|(_, src)| {
let code = strip_comments_and_strings(src);
code.contains(concat!("EditorState::new", "_with_roots("))
|| code.contains(concat!("EditorState::open", "_with_roots("))
})
.map(|(name, _)| name)
.collect();
assert!(
adopters.len() >= 60,
"expected the whole in-process population to have migrated; only \
{} files did",
adopters.len()
);
}

68
tests/common/iso.rs Normal file
View File

@ -0,0 +1,68 @@
// tests/common/iso.rs --- isolated bootstrap roots for integration tests.
//! The integration suite's side of `pmacs::bootstrap`.
//!
//! An integration test links `pmacs` as an ordinary dependency, so it is
//! compiled **without** `cfg(test)` and the `#[cfg(not(test))]` guard in
//! `EditorState::new` is live for it: a raw `EditorState::new()` reads
//! the developer's real `~/.config/pmacs/init.lua` and writes bundled
//! packages into the developer's real `~/.local/share/pmacs`. It cannot
//! fix that by setting an environment variable --- `std::env::set_var`
//! is `unsafe` and `pmacs` is `#![forbid(unsafe_code)]`. So it passes
//! [`roots`] to `EditorState::new_with_roots` instead.
//!
//! See `docs/test-ambient-config-isolation-framing.md`.
//!
//! Included per-file rather than through `mod common;` so a suite that
//! needs no daemon or PTY fixture does not compile them:
//!
//! ```ignore
//! #[path = "common/iso.rs"]
//! mod iso;
//! ```
//!
//! [`roots`] is a **pure function of the build environment** --- no
//! counter, no `OnceLock`. Two copies of this module in one test binary
//! (one via `mod common;`, one via `#[path]`) therefore return the same
//! value rather than racing over a shared counter.
#![allow(dead_code)] // not every including suite uses every helper
use std::path::PathBuf;
use pmacs::bootstrap::BootstrapRoots;
/// The shared isolated base, under Cargo's per-package integration-test
/// temp directory.
///
/// `CARGO_TARGET_TMPDIR` (`target/<profile>/tmp`) rather than `/tmp`:
/// nothing here is ever unlinked --- there is no libtest teardown hook
/// to unlink it from --- so the tree has to live somewhere `cargo clean`
/// owns instead of leaking into the system temp dir once per run.
///
/// Deliberately shared across tests and across test binaries.
/// `materialize_all` is content-gated and idempotent, so after the first
/// construction every later one is a no-op read; a per-test directory
/// would repeat the whole materialization ~330 times per run for no
/// isolation gain (the tree is byte-identical for every caller).
#[must_use]
pub fn base() -> PathBuf {
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("ambient-isolation")
}
/// Isolated bootstrap roots for an in-process editor.
///
/// Pass to `EditorState::new_with_roots` / `open_with_roots`.
#[must_use]
pub fn roots() -> BootstrapRoots {
let base = base();
let roots = BootstrapRoots::isolated_under(&base);
// Create the config dir eagerly. `load_user_config_at` registers it
// on Lua's `package.path` whether or not `init.lua` exists, and a
// suite that later writes a config chunk into it should not have to
// know the layout.
if let Some(dir) = roots.config_dir() {
std::fs::create_dir_all(&dir).expect("create isolated config dir");
}
roots
}

View File

@ -14,8 +14,14 @@
//! - [`daemon`]: `pmacs --daemon` subprocess fixture. First
//! consumer M5.5 acceptance suite; second consumer M10.11
//! doubled-PTY tests.
//! - [`iso`]: isolated bootstrap storage roots, so an in-process
//! editor neither reads the developer's real `init.lua` nor writes
//! into their real data root. Most suites include it directly via
//! `#[path = "common/iso.rs"] mod iso;` rather than through this
//! module; it is re-exported here for `daemon`'s use.
#![allow(dead_code)] // not every integration-test file uses every helper
pub mod daemon;
pub mod iso;
pub mod pty;