707 lines
28 KiB
Rust
707 lines
28 KiB
Rust
// 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 — that the *ambient* constructor is unchanged —
|
||
// deliberately does NOT live here. Asserting it needs an ambient
|
||
// `EditorState::new()`, and an ambient construction in an ordinary
|
||
// parent test reads the developer's real `init.lua` and materializes
|
||
// packages into their real data root: the exact exposure this suite
|
||
// exists to remove, committed by the suite itself. It lives in the
|
||
// re-exec'd positive control below, where the roots are controlled
|
||
// by construction.
|
||
}
|
||
|
||
/// **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.
|
||
///
|
||
/// **This is the suite's only ambient construction, and it runs only as
|
||
/// a re-exec'd child** (the marker gates it), where the roots are
|
||
/// controlled by construction. An ambient `EditorState::new()` in an
|
||
/// ordinary parent test would read the developer's real `init.lua` and
|
||
/// write their real data root — so the one place that legitimately needs
|
||
/// the ambient constructor is also the one place where the environment
|
||
/// has already been redirected. Every ambient claim this suite makes
|
||
/// belongs here for that reason.
|
||
#[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();
|
||
// Relocated from `isolated_construction_is_init_complete`: the
|
||
// ambient constructor is unchanged by this lane and still finishes
|
||
// initialization. Asserting it needs an ambient construction, which
|
||
// is only safe here.
|
||
assert!(
|
||
state.lua_host.is_init_complete(),
|
||
"the ambient constructor must still leave the init phase — the \
|
||
roots parameter changes which directory is read, never whether \
|
||
the block runs"
|
||
);
|
||
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, **with the exact number of sites each is permitted**.
|
||
///
|
||
/// `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.
|
||
///
|
||
/// **The count is the point, not decoration.** A bare file-level
|
||
/// exemption is the weakest form of this ratchet: it licenses the named
|
||
/// file to grow *new* ambient sites forever. That is not hypothetical —
|
||
/// review round 1 of this PR found an ambient `EditorState::new()` in an
|
||
/// ordinary parent test of **this very file**, and the file-level
|
||
/// exemption is precisely what let it through a green ratchet. Every
|
||
/// exemption is now a census entry, so an added site fails even inside
|
||
/// an allowlisted file, and a removed one has to be recorded.
|
||
const AMBIENT_ALLOWLIST: &[(&str, usize)] = &[
|
||
// 19 `new()` + 7 `open(` — the golden journey, every one of them
|
||
// reached only from inside a re-exec'd child. (29 textual
|
||
// occurrences; the scanner drops 2 assertion-message mentions and
|
||
// the assembled `concat!` needle in the self-source check.)
|
||
("journey_acceptance.rs", 26),
|
||
// Exactly one: the re-exec'd positive control.
|
||
("ambient_isolation_acceptance.rs", 1),
|
||
];
|
||
|
||
/// 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 miscounted: 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;
|
||
}
|
||
match AMBIENT_ALLOWLIST.iter().find(|(f, _)| *f == name.as_str()) {
|
||
Some((file, allowed)) => {
|
||
seen_allowlisted.push(file);
|
||
// An allowlisted file is exempted for the sites it was
|
||
// reviewed with, not for any it grows later.
|
||
if hits != *allowed {
|
||
miscounted.push(format!("{name}: {hits} site(s), allowlist says {allowed}"));
|
||
}
|
||
}
|
||
None => offenders.push(format!("{name} ({hits} site(s))")),
|
||
}
|
||
}
|
||
offenders.sort();
|
||
miscounted.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.",
|
||
);
|
||
assert!(
|
||
miscounted.is_empty(),
|
||
"allowlisted files whose ambient site count moved: {miscounted:?}\n\
|
||
MORE than allowed means a new ambient construction slipped into \
|
||
an exempted file — the failure mode a bare file-level exemption \
|
||
cannot see. FEWER means the census is stale; update the count.",
|
||
);
|
||
// 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()
|
||
.map(|(f, _)| *f)
|
||
.filter(|f| !seen_allowlisted.contains(f))
|
||
.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()
|
||
);
|
||
}
|