Merge pull request #206 from levineuwirth/test-ambient-isolation-impl
test: isolate integration tests from the developer's ambient storage roots
This commit is contained in:
commit
28f878b734
|
|
@ -414,13 +414,33 @@ compatible.
|
|||
- **DAP waits for Stage 2, not Stage 1** — that dependency is now
|
||||
satisfied.
|
||||
|
||||
## Test ambient-root isolation — FRAMING OPEN, revision 4
|
||||
## Test ambient-root isolation — IMPLEMENTATION OPEN
|
||||
|
||||
- **Branch `test-ambient-config-isolation`**, worktree
|
||||
`../pmacs-test-isolation`, based on `githubsucks/main` @ `4cd4a7b`.
|
||||
**Framing only; no code, no PR yet.**
|
||||
`docs/test-ambient-config-isolation-framing.md` revision 4, three review
|
||||
rounds closed (eight blocking, six major, all accepted).
|
||||
- **Framing MERGED (#201)**;
|
||||
`docs/test-ambient-config-isolation-framing.md` **revision 5**
|
||||
(revision 4 approved after three review rounds — eight blocking, six
|
||||
major, all accepted; revision 5 rides the implementation PR and records
|
||||
findings, not a new design round).
|
||||
- **PR #206 OPEN**, one review round closed. **§7's branch plan was
|
||||
consciously exceeded**: the whole-corpus migration rides this PR rather
|
||||
than a follow-up lane, because splitting would either leave 65 suites
|
||||
writing the real data root or ship acceptance 12's ratchet with a
|
||||
~65-file allowlist that does not ratchet. Recorded in framing revision
|
||||
5; accepted in review.
|
||||
- **Implementation branch `test-ambient-isolation-impl`**, worktree
|
||||
`../pmacs-test-isolation-impl`, based on `githubsucks/main` @
|
||||
`54a092e`. The framing-only worktree `../pmacs-test-isolation`
|
||||
(branch `test-ambient-config-isolation`) is spent — its doc is on
|
||||
`main`.
|
||||
- **What landed on the branch.** `pmacs::bootstrap::BootstrapRoots`
|
||||
(config/data/state/cache, `ambient()` for production), reachable from
|
||||
`EditorState::new_with_roots` **and** `open_with_roots` and consulted
|
||||
again by `install_state_dirs`; all 342 in-process construction sites
|
||||
in 65 files migrated; `journey_acceptance` keeps the ambient `open`
|
||||
and re-execs itself per test with controlled roots instead; the shared
|
||||
daemon and PTY spawners now set all five storage variables;
|
||||
`tests/ambient_isolation_acceptance.rs` carries the hostile-environment
|
||||
proof and the adoption ratchet.
|
||||
- **What it is.** Integration tests use the developer's real ambient
|
||||
roots. `#[cfg(not(test))]` guards config loading against the crate's
|
||||
own unit tests only, so the **65** files in `tests/` that construct an
|
||||
|
|
@ -455,9 +475,9 @@ compatible.
|
|||
|
||||
```sh
|
||||
git fetch githubsucks
|
||||
git worktree add ../pmacs-test-isolation \
|
||||
-b test-ambient-config-isolation \
|
||||
githubsucks/test-ambient-config-isolation
|
||||
git worktree add ../pmacs-test-isolation-impl \
|
||||
-b test-ambient-isolation-impl \
|
||||
githubsucks/test-ambient-isolation-impl
|
||||
```
|
||||
|
||||
## Reap-ledger silent failures — MERGED (#202); kept for its parked follow-ons
|
||||
|
|
|
|||
|
|
@ -1639,6 +1639,35 @@ git diff --check
|
|||
Machine-specific caveats — re-verify on a machine you haven't used
|
||||
before trusting them:
|
||||
|
||||
- **Ambient storage roots: control all FIVE, not four.** Until the
|
||||
ambient-root isolation lane lands
|
||||
(`docs/test-ambient-config-isolation-framing.md`), the ~96 integration
|
||||
suites read the developer's real `~/.config/pmacs/init.lua` and
|
||||
**write** bundled packages into the real data root:
|
||||
`#[cfg(not(test))]` guards the crate's own unit tests only, and
|
||||
`EditorState::new` materializes packages outside every `cfg` guard.
|
||||
A local full-suite run therefore needs, all pointed at a fresh
|
||||
directory:
|
||||
|
||||
```
|
||||
XDG_CONFIG_HOME XDG_DATA_HOME XDG_STATE_HOME XDG_CACHE_HOME
|
||||
PMACS_STATE_HOME
|
||||
```
|
||||
|
||||
**The fifth is not redundant.** `PMACS_STATE_HOME` outranks
|
||||
`XDG_STATE_HOME` (`src/state.rs`), so redirecting the four XDG
|
||||
variables on a machine that exports it leaves the real state root
|
||||
live. `HOME` is deliberately left alone: it is the fallback for the
|
||||
XDG roots (already covered once they are set) and separately drives
|
||||
`~`-expansion, which `find_file_acceptance` pins on purpose.
|
||||
A run isolating only `XDG_CONFIG_HOME` stops the `init.lua` reads and
|
||||
still writes through the real data root — every local gate run in this
|
||||
repo before 2026-07-31 had that hole.
|
||||
|
||||
**After the lane lands** the in-process population isolates itself
|
||||
through `EditorState::new_with_roots(&crate::iso::roots())` and the
|
||||
five variables are belt-and-braces rather than required.
|
||||
|
||||
- **basedpyright**: the desktop binary was **never broken** — this was a
|
||||
real code defect, diagnosed and fixed 2026-07-29 (see §5, "A `Drop`
|
||||
body runs before its fields"). `RuntimeHandles::drop` joined its reader
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# Framing — integration tests use the developer's real ambient roots
|
||||
|
||||
**Revision 4.** Status: awaiting review round 4. Proposed lane:
|
||||
`test-ambient-config-isolation`, worktree `../pmacs-test-isolation`,
|
||||
based on `githubsucks/main` @ `4cd4a7b` (a reading; re-measure at branch
|
||||
time).
|
||||
**Revision 5.** Status: implemented on branch
|
||||
`test-ambient-isolation-impl` (worktree `../pmacs-test-isolation-impl`,
|
||||
based on `githubsucks/main` @ `54a092e`), PR #206. Revision 4 was
|
||||
approved after three review rounds and merged as #201; revision 5 records
|
||||
implementation findings and one **deliberate departure from §7's branch
|
||||
plan**, not a new design round.
|
||||
|
||||
**The suite is green in CI and red on a developer machine that has a
|
||||
real `~/.config/pmacs/init.lua`.** Not flaky — deterministic, and
|
||||
|
|
@ -16,6 +18,51 @@ directory**. The lane is therefore about *ambient roots*, not about
|
|||
|
||||
## Revision history
|
||||
|
||||
**Revision 4 → 5**, at implementation. No design changed; two things are
|
||||
recorded that a later reader would otherwise have to reconstruct.
|
||||
|
||||
### The §7 branch plan was consciously exceeded
|
||||
|
||||
**§7 said: classification first and alone, "its answer belongs in review
|
||||
before any mechanical edit rides on it."** The classification came back
|
||||
at **342 in-process construction sites across 66 of 97 files** — large
|
||||
enough that §7's shape would naturally suggest splitting the mechanical
|
||||
migration into its own lane. **It was not split, and that is deliberate.**
|
||||
|
||||
The alternatives were both worse, and both worse in the way this lane
|
||||
exists to prevent:
|
||||
|
||||
- **Split, migrating nothing now.** The seam would land while 65 files
|
||||
kept reading the developer's real `init.lua` and writing their real
|
||||
data root. The lane's own defect would survive its own PR.
|
||||
- **Split, with a broad temporary allowlist.** Acceptance 12's ratchet
|
||||
would ship exempting ~65 files. A ratchet whose allowlist is most of
|
||||
the corpus does not ratchet; it records. And the exemption would have
|
||||
to be removed later by the same reviewer who granted it, with nothing
|
||||
failing in the meantime to remind anyone.
|
||||
|
||||
So the whole-corpus migration is the atomic remediation and rides this
|
||||
PR. §7's *ordering* is honoured — the classification is the first commit,
|
||||
before any mechanical edit — while its implied *scoping* is not. The
|
||||
qualification this leaves on acceptance 12 (the allowlist is narrow
|
||||
because everything else moved, not because the change was small) was
|
||||
raised in the PR and accepted in review round 1.
|
||||
|
||||
### The exemption shape acceptance 12 needs
|
||||
|
||||
Review round 1 found an ambient `EditorState::new()` in an ordinary
|
||||
parent test of the isolation suite itself — the exposure, committed by
|
||||
the suite written to remove it. **A file-level allowlist did not catch
|
||||
it**, because the file was already exempted for its re-exec'd positive
|
||||
control, and a bare exemption licenses a named file to grow new ambient
|
||||
sites indefinitely.
|
||||
|
||||
The fix is that **every exemption carries its exact site count**, so an
|
||||
added site fails even inside an exempted file. That is the shape
|
||||
acceptance 12 needs; "a narrow, named allowlist" is not sufficient on its
|
||||
own, because narrowness constrains which files are exempt and says
|
||||
nothing about how far each exemption stretches.
|
||||
|
||||
**Revision 3 → 4**, after review round 3 (one blocking, two major). All
|
||||
three accepted.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,391 @@
|
|||
// src/bootstrap.rs --- explicit bootstrap storage roots.
|
||||
|
||||
//! Where pmacs stores things at startup, as a value rather than as an
|
||||
//! ambient property of the process environment.
|
||||
//!
|
||||
//! # Why this exists
|
||||
//!
|
||||
//! `EditorState::new` resolves two storage roots from the environment
|
||||
//! before it returns:
|
||||
//!
|
||||
//! * the **data** root, which
|
||||
//! [`crate::builtin_packages::bundled_runtime_dir`] resolves from
|
||||
//! `XDG_DATA_HOME` (else `$HOME/.local/share`) and which
|
||||
//! [`crate::builtin_packages::materialize_all`] then **writes into**
|
||||
//! --- unconditionally, outside any `cfg` guard; and
|
||||
//! * the **config** root, which [`crate::config::user_config_dir`]
|
||||
//! resolves from `XDG_CONFIG_HOME` (else `$HOME/.config`) and from
|
||||
//! which `init.lua` is read.
|
||||
//!
|
||||
//! An integration test in `tests/` links this crate as an ordinary
|
||||
//! dependency, so it is compiled **without** `cfg(test)`: the
|
||||
//! `#[cfg(not(test))]` guard around config loading is inactive for
|
||||
//! every one of them. They read the developer's real `init.lua` and
|
||||
//! write into the developer's real data root.
|
||||
//!
|
||||
//! They cannot fix that themselves. `std::env::set_var` has been
|
||||
//! `unsafe` since Rust 2024 and this crate is `#![forbid(unsafe_code)]`
|
||||
//! --- the same constraint that produced
|
||||
//! [`crate::packages::installer::Installer::with_install_root_override`]
|
||||
//! and [`crate::lua_bindings::PackageInstallOverride`]. So isolation has
|
||||
//! to arrive as a **parameter**, which is what this type is.
|
||||
//!
|
||||
//! # Contract
|
||||
//!
|
||||
//! [`BootstrapRoots::ambient()`] is production: every root stays `None`
|
||||
//! and every resolution goes to the environment exactly as before. A
|
||||
//! root that is `Some` replaces the environment lookup for that root and
|
||||
//! **only** that root.
|
||||
//!
|
||||
//! # Scope: storage roots only
|
||||
//!
|
||||
//! This type covers the four roots that decide where pmacs *stores*
|
||||
//! things: config, data, state and cache. It deliberately does not
|
||||
//! cover:
|
||||
//!
|
||||
//! * **`HOME`'s non-storage semantics.** `expand_tilde`
|
||||
//! ([`crate::editor_core`]) resolves a leading `~` for ordinary path
|
||||
//! entry, and `tests/find_file_acceptance.rs` consumes `HOME` on
|
||||
//! purpose to pin that expansion. Redirecting a storage root is the
|
||||
//! right fix for a storage root and the wrong fix for a
|
||||
//! path-expansion root.
|
||||
//! * **`XDG_RUNTIME_DIR`**, which addresses sockets rather than stored
|
||||
//! data.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The bootstrap storage roots an [`crate::editor::EditorState`] is
|
||||
/// constructed against.
|
||||
///
|
||||
/// Each field is the *base* directory --- the value `XDG_<X>_HOME`
|
||||
/// would hold --- not the `pmacs/` subdirectory under it. `None` means
|
||||
/// "resolve from the environment", which is what production does.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct BootstrapRoots {
|
||||
config: Option<PathBuf>,
|
||||
data: Option<PathBuf>,
|
||||
state: Option<PathBuf>,
|
||||
cache: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// The version-keyed leaf `bundled_runtime_dir` materializes into.
|
||||
fn bundled_leaf() -> String {
|
||||
format!("v{}", env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
impl BootstrapRoots {
|
||||
/// Production: every root resolves from the process environment.
|
||||
#[must_use]
|
||||
pub fn ambient() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Every storage root redirected under `base`, into sibling
|
||||
/// `config/`, `data/`, `state/` and `cache/` directories.
|
||||
///
|
||||
/// The layout mirrors the XDG one so a caller that also spawns a
|
||||
/// child process can hand the same four paths to `XDG_CONFIG_HOME`,
|
||||
/// `XDG_DATA_HOME`, `XDG_STATE_HOME` and `XDG_CACHE_HOME` (plus
|
||||
/// `PMACS_STATE_HOME`, which outranks `XDG_STATE_HOME`) and get the
|
||||
/// same tree from an in-process and a spawned editor.
|
||||
#[must_use]
|
||||
pub fn isolated_under(base: &Path) -> Self {
|
||||
Self {
|
||||
config: Some(base.join("config")),
|
||||
data: Some(base.join("data")),
|
||||
state: Some(base.join("state")),
|
||||
cache: Some(base.join("cache")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder: redirect the config root (the `XDG_CONFIG_HOME` value).
|
||||
#[must_use]
|
||||
pub fn with_config_root(mut self, root: PathBuf) -> Self {
|
||||
self.config = Some(root);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: redirect the data root (the `XDG_DATA_HOME` value).
|
||||
#[must_use]
|
||||
pub fn with_data_root(mut self, root: PathBuf) -> Self {
|
||||
self.data = Some(root);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: redirect the state root (the `XDG_STATE_HOME` value).
|
||||
#[must_use]
|
||||
pub fn with_state_root(mut self, root: PathBuf) -> Self {
|
||||
self.state = Some(root);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: redirect the cache root (the `XDG_CACHE_HOME` value).
|
||||
#[must_use]
|
||||
pub fn with_cache_root(mut self, root: PathBuf) -> Self {
|
||||
self.cache = Some(root);
|
||||
self
|
||||
}
|
||||
|
||||
/// True when nothing is redirected --- i.e. this is production's
|
||||
/// [`Self::ambient`].
|
||||
#[must_use]
|
||||
pub fn is_ambient(&self) -> bool {
|
||||
self.config.is_none() && self.data.is_none() && self.state.is_none() && self.cache.is_none()
|
||||
}
|
||||
|
||||
/// The config *base*, if redirected.
|
||||
#[must_use]
|
||||
pub fn config_root(&self) -> Option<&Path> {
|
||||
self.config.as_deref()
|
||||
}
|
||||
|
||||
/// The data *base*, if redirected.
|
||||
#[must_use]
|
||||
pub fn data_root(&self) -> Option<&Path> {
|
||||
self.data.as_deref()
|
||||
}
|
||||
|
||||
/// The state *base*, if redirected.
|
||||
#[must_use]
|
||||
pub fn state_root(&self) -> Option<&Path> {
|
||||
self.state.as_deref()
|
||||
}
|
||||
|
||||
/// The cache *base*, if redirected.
|
||||
#[must_use]
|
||||
pub fn cache_root(&self) -> Option<&Path> {
|
||||
self.cache.as_deref()
|
||||
}
|
||||
|
||||
/// The directory `init.lua` is read from --- `<config>/pmacs`, the
|
||||
/// same shape [`crate::config::user_config_dir`] builds.
|
||||
#[must_use]
|
||||
pub fn config_dir(&self) -> Option<PathBuf> {
|
||||
self.config.as_ref().map(|p| p.join("pmacs"))
|
||||
}
|
||||
|
||||
/// Where bundled packages are materialized ---
|
||||
/// `<data>/pmacs/builtin-packages/v<crate-version>`, the same shape
|
||||
/// [`crate::builtin_packages::bundled_runtime_dir`] builds.
|
||||
#[must_use]
|
||||
pub fn bundled_runtime_dir(&self) -> Option<PathBuf> {
|
||||
self.data.as_ref().map(|p| {
|
||||
p.join("pmacs")
|
||||
.join("builtin-packages")
|
||||
.join(bundled_leaf())
|
||||
})
|
||||
}
|
||||
|
||||
/// The user-scope package install root --- `<data>/pmacs/packages`.
|
||||
#[must_use]
|
||||
pub fn package_install_root(&self) -> Option<PathBuf> {
|
||||
self.data.as_ref().map(|p| p.join("pmacs").join("packages"))
|
||||
}
|
||||
|
||||
/// The package fetcher's bare-mirror cache --- `<cache>/pmacs/git`.
|
||||
#[must_use]
|
||||
pub fn package_cache_dir(&self) -> Option<PathBuf> {
|
||||
self.cache.as_ref().map(|p| p.join("pmacs").join("git"))
|
||||
}
|
||||
|
||||
/// The editor state directory --- `<state>/pmacs`, the same shape
|
||||
/// [`crate::state::user_state_dir`] builds.
|
||||
#[must_use]
|
||||
pub fn state_dir(&self) -> Option<PathBuf> {
|
||||
self.state.as_ref().map(|p| p.join("pmacs"))
|
||||
}
|
||||
|
||||
/// The minibuffer history directory --- `<state>/pmacs/history`.
|
||||
#[must_use]
|
||||
pub fn history_dir(&self) -> Option<PathBuf> {
|
||||
self.state_dir().map(|d| d.join("history"))
|
||||
}
|
||||
|
||||
/// The environment a **child** `pmacs` process must be given so it
|
||||
/// resolves the same roots this value names.
|
||||
///
|
||||
/// An in-process caller passes the value; a caller that spawns
|
||||
/// `pmacs --daemon` (or re-execs a test binary) cannot, and has to
|
||||
/// go through the environment instead. This is that translation, so
|
||||
/// the two paths cannot drift.
|
||||
///
|
||||
/// **Five variables, not four.** `PMACS_STATE_HOME` outranks
|
||||
/// `XDG_STATE_HOME` ([`crate::state::user_state_dir`]), so a child
|
||||
/// given only the four XDG variables still resolves the *inherited*
|
||||
/// `PMACS_STATE_HOME` if the launching environment exports one --- a
|
||||
/// hole that is invisible on a machine that does not.
|
||||
///
|
||||
/// A root left ambient emits no variable, so the child inherits it.
|
||||
#[must_use]
|
||||
pub fn child_env(&self) -> Vec<(&'static str, PathBuf)> {
|
||||
let mut out = Vec::with_capacity(5);
|
||||
if let Some(p) = &self.config {
|
||||
out.push(("XDG_CONFIG_HOME", p.clone()));
|
||||
}
|
||||
if let Some(p) = &self.data {
|
||||
out.push(("XDG_DATA_HOME", p.clone()));
|
||||
}
|
||||
if let Some(p) = &self.state {
|
||||
out.push(("XDG_STATE_HOME", p.clone()));
|
||||
out.push(("PMACS_STATE_HOME", p.clone()));
|
||||
}
|
||||
if let Some(p) = &self.cache {
|
||||
out.push(("XDG_CACHE_HOME", p.clone()));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ambient_redirects_nothing() {
|
||||
let roots = BootstrapRoots::ambient();
|
||||
assert!(roots.is_ambient());
|
||||
assert_eq!(roots.config_dir(), None);
|
||||
assert_eq!(roots.bundled_runtime_dir(), None);
|
||||
assert_eq!(roots.state_dir(), None);
|
||||
assert_eq!(roots.package_cache_dir(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_under_mirrors_the_xdg_layout() {
|
||||
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
|
||||
assert!(!roots.is_ambient());
|
||||
assert_eq!(
|
||||
roots.config_dir().unwrap(),
|
||||
Path::new("/scratch/config/pmacs")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.bundled_runtime_dir().unwrap(),
|
||||
Path::new("/scratch/data/pmacs/builtin-packages").join(bundled_leaf())
|
||||
);
|
||||
assert_eq!(
|
||||
roots.package_install_root().unwrap(),
|
||||
Path::new("/scratch/data/pmacs/packages")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.package_cache_dir().unwrap(),
|
||||
Path::new("/scratch/cache/pmacs/git")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.state_dir().unwrap(),
|
||||
Path::new("/scratch/state/pmacs")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.history_dir().unwrap(),
|
||||
Path::new("/scratch/state/pmacs/history")
|
||||
);
|
||||
}
|
||||
|
||||
/// A builder that sets one root leaves the other three ambient ---
|
||||
/// the "only that root" half of the contract.
|
||||
#[test]
|
||||
fn a_single_builder_leaves_the_other_roots_ambient() {
|
||||
let roots = BootstrapRoots::ambient().with_config_root(PathBuf::from("/only/config"));
|
||||
assert!(!roots.is_ambient());
|
||||
assert_eq!(roots.config_dir().unwrap(), Path::new("/only/config/pmacs"));
|
||||
assert_eq!(roots.bundled_runtime_dir(), None);
|
||||
assert_eq!(roots.state_dir(), None);
|
||||
assert_eq!(roots.package_cache_dir(), None);
|
||||
}
|
||||
|
||||
/// The five-variable contract, asserted as content: naming only the
|
||||
/// four XDG variables leaves `PMACS_STATE_HOME` --- which outranks
|
||||
/// `XDG_STATE_HOME` --- pointing wherever the launching environment
|
||||
/// left it.
|
||||
#[test]
|
||||
fn child_env_names_all_five_storage_variables() {
|
||||
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
|
||||
let env = roots.child_env();
|
||||
let names: Vec<&str> = env.iter().map(|(k, _)| *k).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_STATE_HOME",
|
||||
"PMACS_STATE_HOME",
|
||||
"XDG_CACHE_HOME",
|
||||
]
|
||||
);
|
||||
let value = |name: &str| {
|
||||
env.iter()
|
||||
.find(|(k, _)| *k == name)
|
||||
.map(|(_, v)| v.clone())
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(value("XDG_CONFIG_HOME"), Path::new("/scratch/config"));
|
||||
assert_eq!(value("XDG_DATA_HOME"), Path::new("/scratch/data"));
|
||||
assert_eq!(value("XDG_STATE_HOME"), Path::new("/scratch/state"));
|
||||
assert_eq!(value("PMACS_STATE_HOME"), Path::new("/scratch/state"));
|
||||
assert_eq!(value("XDG_CACHE_HOME"), Path::new("/scratch/cache"));
|
||||
}
|
||||
|
||||
/// An ambient root emits no variable — the child inherits it. A
|
||||
/// blanket five-variable emission would silently redirect roots the
|
||||
/// caller deliberately left alone.
|
||||
#[test]
|
||||
fn child_env_emits_nothing_for_ambient_roots() {
|
||||
assert!(BootstrapRoots::ambient().child_env().is_empty());
|
||||
let only_config = BootstrapRoots::ambient().with_config_root(PathBuf::from("/only/config"));
|
||||
assert_eq!(
|
||||
only_config.child_env(),
|
||||
vec![("XDG_CONFIG_HOME", PathBuf::from("/only/config"))]
|
||||
);
|
||||
}
|
||||
|
||||
/// `child_env` and the in-process resolvers must describe the same
|
||||
/// tree: the spawned daemon and the in-process editor are both
|
||||
/// supposed to land in the isolated roots, and a mismatch would let
|
||||
/// one of them escape while the other looked fine.
|
||||
#[test]
|
||||
fn child_env_agrees_with_the_in_process_resolvers() {
|
||||
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
|
||||
let env = roots.child_env();
|
||||
let value = |name: &str| {
|
||||
env.iter()
|
||||
.find(|(k, _)| *k == name)
|
||||
.map(|(_, v)| v.clone())
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
roots.config_dir().unwrap(),
|
||||
value("XDG_CONFIG_HOME").join("pmacs")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.package_install_root().unwrap(),
|
||||
value("XDG_DATA_HOME").join("pmacs").join("packages")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.state_dir().unwrap(),
|
||||
value("PMACS_STATE_HOME").join("pmacs")
|
||||
);
|
||||
assert_eq!(
|
||||
roots.package_cache_dir().unwrap(),
|
||||
value("XDG_CACHE_HOME").join("pmacs").join("git")
|
||||
);
|
||||
}
|
||||
|
||||
/// The isolated bundled dir must agree with the ambient resolver's
|
||||
/// shape, version leaf included: a mismatch would make an isolated
|
||||
/// editor materialize somewhere production never looks.
|
||||
#[test]
|
||||
fn bundled_leaf_matches_the_ambient_resolver() {
|
||||
let roots = BootstrapRoots::isolated_under(Path::new("/scratch"));
|
||||
let isolated = roots.bundled_runtime_dir().unwrap();
|
||||
// `bundled_runtime_dir()` reads the live environment, so compare
|
||||
// only the tail that does not depend on it.
|
||||
let ambient = crate::builtin_packages::bundled_runtime_dir();
|
||||
let tail = |p: &Path| {
|
||||
p.components()
|
||||
.rev()
|
||||
.take(3)
|
||||
.map(|c| c.as_os_str().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(tail(&isolated), tail(&ambient));
|
||||
}
|
||||
}
|
||||
123
src/editor.rs
123
src/editor.rs
|
|
@ -273,6 +273,14 @@ pub struct EditorState {
|
|||
/// steal or clear another's in-flight gesture, and concurrent drags
|
||||
/// are perfectly legal.
|
||||
window_drag: HashMap<FrontendId, WindowDragState>,
|
||||
/// The bootstrap storage roots this session was constructed
|
||||
/// against (`docs/test-ambient-config-isolation-framing.md`).
|
||||
///
|
||||
/// [`BootstrapRoots::ambient`] in production. Retained past
|
||||
/// construction because [`Self::install_state_dirs`] resolves a root
|
||||
/// too and runs *after* the constructor --- resolving it from the
|
||||
/// environment there would reopen the hole the constructor closed.
|
||||
bootstrap_roots: crate::bootstrap::BootstrapRoots,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -345,6 +353,24 @@ impl EditorState {
|
|||
/// Panics only if Lua initialization or the builtin command/keymap
|
||||
/// chunks fail to load --- both indicate broken builds.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_roots(&crate::bootstrap::BootstrapRoots::ambient())
|
||||
}
|
||||
|
||||
/// Construct a fresh editor against explicit bootstrap storage
|
||||
/// roots (`docs/test-ambient-config-isolation-framing.md` §1.4).
|
||||
///
|
||||
/// [`Self::new`] is this with [`BootstrapRoots::ambient`], so
|
||||
/// production behaviour is unchanged. An integration test passes
|
||||
/// redirected roots instead: it links this crate without
|
||||
/// `cfg(test)`, so the guard below is live for it, and without a
|
||||
/// parameter it would read the developer's real `init.lua` and
|
||||
/// write bundled packages into the developer's real data root.
|
||||
/// `std::env::set_var` is not an alternative --- it is `unsafe` and
|
||||
/// this crate is `#![forbid(unsafe_code)]`.
|
||||
///
|
||||
/// [`BootstrapRoots::ambient`]: crate::bootstrap::BootstrapRoots::ambient
|
||||
#[must_use]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "linear bootstrap sequence: registry → core → LuaHost → \
|
||||
|
|
@ -352,7 +378,7 @@ impl EditorState {
|
|||
Splitting into helpers fragments the wiring without removing \
|
||||
any single decision the reader needs to follow."
|
||||
)]
|
||||
pub fn new() -> Self {
|
||||
pub fn new_with_roots(roots: &crate::bootstrap::BootstrapRoots) -> Self {
|
||||
// Build the buffer registry first so EditorCore and LuaHost
|
||||
// share the same `Rc`. Both reach buffers through this handle;
|
||||
// multi-window dispatch (T M2.8) requires that ids resolve to
|
||||
|
|
@ -738,9 +764,34 @@ impl EditorState {
|
|||
// Depends on `pmacs.buffer.add_intercept` (T M6.4 Stage 1)
|
||||
// and `pmacs.ansi.parser()` (T M6.4 Stage 2), both available
|
||||
// by the time `attach_editor` returns above.
|
||||
let bundled_root = crate::builtin_packages::bundled_runtime_dir();
|
||||
//
|
||||
// `materialize_all` CREATES DIRECTORIES AND WRITES FILES, and
|
||||
// it is outside every `cfg` guard — so this, not config
|
||||
// loading, is the write half of the ambient-roots exposure
|
||||
// (`docs/test-ambient-config-isolation-framing.md` §1.6). The
|
||||
// redirected root is consulted first for exactly that reason.
|
||||
let bundled_root = roots
|
||||
.bundled_runtime_dir()
|
||||
.unwrap_or_else(crate::builtin_packages::bundled_runtime_dir);
|
||||
let bundled_packages = crate::builtin_packages::materialize_all(&bundled_root)
|
||||
.expect("materialize bundled packages");
|
||||
// Redirected roots also redirect where `pmacs.packages.install`
|
||||
// would later fetch and install, so a caller that isolated its
|
||||
// storage cannot reach the real `$XDG_CACHE_HOME/pmacs/git` or
|
||||
// `$XDG_DATA_HOME/pmacs/packages` through Lua either. Installed
|
||||
// before user config runs, so an `init.lua` that installs a
|
||||
// package lands inside the isolated tree. Production leaves the
|
||||
// slot empty (ambient roots take this branch not at all).
|
||||
if !roots.is_ambient() {
|
||||
let mut override_ = crate::lua_bindings::PackageInstallOverride::new();
|
||||
if let Some(cache) = roots.package_cache_dir() {
|
||||
override_ = override_.with_cache_dir(cache);
|
||||
}
|
||||
if let Some(install) = roots.package_install_root() {
|
||||
override_ = override_.with_user_install_root(install);
|
||||
}
|
||||
lua_host.set_package_install_override(override_);
|
||||
}
|
||||
{
|
||||
let slot = lua_host
|
||||
.lua()
|
||||
|
|
@ -772,17 +823,43 @@ impl EditorState {
|
|||
// to exercise config loading do so explicitly via
|
||||
// [`crate::config::load_user_config_at`].
|
||||
//
|
||||
// **`cfg(test)` covers the lib's unit tests and NOTHING ELSE.**
|
||||
// An integration test in `tests/` links this crate as an
|
||||
// ordinary dependency, compiled without `cfg(test)`, so this
|
||||
// block is fully live for all ~96 of them: `cargo test --lib` is
|
||||
// protected, `cargo test --test <name>` is not. That is what the
|
||||
// `roots` parameter is for — an integration test redirects the
|
||||
// config root instead of relying on a guard that does not reach
|
||||
// it. (Nor is this the only ambient root: the bundled-package
|
||||
// materialization above runs unconditionally and WRITES.) See
|
||||
// `docs/test-ambient-config-isolation-framing.md` §1.2, §1.6.
|
||||
//
|
||||
// The guard is deliberately NOT widened to cover integration
|
||||
// tests: production deciding it is under test is how a suite
|
||||
// passes against behaviour production never runs (Q#TI1).
|
||||
//
|
||||
// The init-complete flip happens here too so lifecycle-gated
|
||||
// Lua APIs (e.g. `pmacs.attach`, M5.6d+) become inert after
|
||||
// user config returns. Tests that need post-init semantics flip
|
||||
// the flag explicitly via [`crate::lua::LuaHost::set_init_complete`];
|
||||
// see the option-(A) discussion in the M5.6c survey.
|
||||
//
|
||||
// Redirected roots change *which directory* is read, never
|
||||
// *whether* the block runs: `tests/m8_2_acceptance.rs:75`
|
||||
// documents its dependence on integration-test construction
|
||||
// finishing init-complete, so skipping the block for isolated
|
||||
// construction would leave every such test permanently in the
|
||||
// init phase (framing §1.8).
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
crate::config::load_user_config(&mut lua_host);
|
||||
match roots.config_dir() {
|
||||
Some(dir) => crate::config::load_user_config_at(&mut lua_host, &dir),
|
||||
None => crate::config::load_user_config(&mut lua_host),
|
||||
}
|
||||
lua_host.set_init_complete();
|
||||
}
|
||||
Self {
|
||||
bootstrap_roots: roots.clone(),
|
||||
core,
|
||||
lua_host,
|
||||
dispatchers: HashMap::new(),
|
||||
|
|
@ -879,10 +956,24 @@ impl EditorState {
|
|||
/// `~/.local/state/pmacs`. Honors the `PMACS_STATE_HOME` override
|
||||
/// (see [`crate::state::user_state_dir`]).
|
||||
pub fn install_state_dirs(&self) {
|
||||
if let Some(dir) = crate::minibuffer::user_history_dir() {
|
||||
// Redirected roots win over the environment here too. This runs
|
||||
// after construction, so resolving from the environment would
|
||||
// hand an isolated session the developer's real state dir --- and
|
||||
// `PMACS_STATE_HOME` outranks `XDG_STATE_HOME`, so an
|
||||
// environment-side fix would have to cover five variables, not
|
||||
// four (framing §1.6a).
|
||||
let history = self
|
||||
.bootstrap_roots
|
||||
.history_dir()
|
||||
.or_else(crate::minibuffer::user_history_dir);
|
||||
if let Some(dir) = history {
|
||||
self.core.borrow_mut().minibuffer.history_dir = Some(dir);
|
||||
}
|
||||
if let Some(dir) = crate::state::user_state_dir() {
|
||||
let state = self
|
||||
.bootstrap_roots
|
||||
.state_dir()
|
||||
.or_else(crate::state::user_state_dir);
|
||||
if let Some(dir) = state {
|
||||
self.lua_host
|
||||
.lua()
|
||||
.set_app_data(crate::lua_bindings::StateDir(dir));
|
||||
|
|
@ -1029,7 +1120,27 @@ impl EditorState {
|
|||
behavioral gain"
|
||||
)]
|
||||
pub fn open(path: PathBuf) -> io::Result<Self> {
|
||||
let mut state = Self::new();
|
||||
Self::open_with_roots(path, &crate::bootstrap::BootstrapRoots::ambient())
|
||||
}
|
||||
|
||||
/// [`Self::open`] against explicit bootstrap storage roots.
|
||||
///
|
||||
/// `open` calls `Self::new()` internally, so a roots parameter on
|
||||
/// the constructor alone leaves every open-path test ambient
|
||||
/// (`docs/test-ambient-config-isolation-framing.md` §1.7). The two
|
||||
/// entry points therefore gain the parameter together, and `open`
|
||||
/// stays a thin ambient caller of this — the golden-journey ratchet
|
||||
/// requires that exact public entry point to have a production
|
||||
/// caller shape.
|
||||
#[allow(
|
||||
clippy::needless_pass_by_value,
|
||||
reason = "mirrors `open`'s stable signature; see the note there"
|
||||
)]
|
||||
pub fn open_with_roots(
|
||||
path: PathBuf,
|
||||
roots: &crate::bootstrap::BootstrapRoots,
|
||||
) -> io::Result<Self> {
|
||||
let mut state = Self::new_with_roots(roots);
|
||||
let resolved = state
|
||||
.core
|
||||
.borrow_mut()
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub mod attach_dispatch;
|
|||
pub mod attach_reconnect;
|
||||
pub mod audit;
|
||||
pub mod autosave;
|
||||
pub mod bootstrap;
|
||||
pub mod buffer;
|
||||
pub mod buffer_registry;
|
||||
pub mod builtin_packages;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,706 @@
|
|||
// 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()
|
||||
);
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ fn status(s: &EditorState) -> String {
|
|||
|
||||
/// Fresh editor whose active scratch buffer holds `body`, cursor at 0.
|
||||
fn editor_with(body: &str) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
if !body.is_empty() {
|
||||
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
|
||||
}
|
||||
|
|
@ -503,3 +503,10 @@ fn isearch_ret_accepts_instead_of_inserting() {
|
|||
"RET during isearch accepts; no newline is inserted"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -114,7 +114,7 @@ fn status(s: &EditorState) -> String {
|
|||
/// Fresh scratch-buffer editor whose buffer holds `body`, cursor at 0.
|
||||
/// No state dir / no files: scratch pairing uses the `default` set.
|
||||
fn editor_with(body: &str) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
if !body.is_empty() {
|
||||
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
|
||||
}
|
||||
|
|
@ -1378,3 +1378,10 @@ fn relocated_closer_first_did_change_carries_the_complete_effective_text() {
|
|||
text in the first didChange — never an opener-only intermediate"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -861,3 +861,10 @@ fn before_quit_sweeps_synchronously_without_vetoing() {
|
|||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const COLS: u32 = 60;
|
|||
const AREA_ROWS: u32 = ROWS - 1;
|
||||
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
// Geometry is authoritative state, and a grid frontend's real frame
|
||||
// size IS its declaration. Every test that does not render declares
|
||||
|
|
@ -2571,7 +2571,7 @@ fn panel_hidden_never_describes_a_panel_that_no_longer_exists() {
|
|||
|
||||
#[test]
|
||||
fn unknown_geometry_is_not_twenty_four_by_eighty() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
let fid = FrontendId(77);
|
||||
attach_frontend(&s, fid, false);
|
||||
|
|
@ -2619,3 +2619,10 @@ fn cell_coord_helper_is_used() {
|
|||
// Keeps the CellCoord import honest for grid assertions above.
|
||||
assert_eq!(CellCoord::new(1, 2).row, 1);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const ROWS: u32 = 24;
|
|||
const COLS: u32 = 60;
|
||||
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
|
||||
s
|
||||
|
|
@ -896,3 +896,10 @@ fn a_provider_closing_the_document_split_still_clears_the_statusline() {
|
|||
the phase-1 document identity, even when a callback closed that window; got {msgs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl Session {
|
|||
/// A semantic, panel-capable frontend with one document window and
|
||||
/// no geometry declared yet.
|
||||
fn new() -> Self {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
let document = {
|
||||
let mut core = state.core.borrow_mut();
|
||||
|
|
@ -659,7 +659,7 @@ fn a2b1_a_rejected_declaration_reconciles_nothing() {
|
|||
fn a2b1_grid_allocator_exhaustion_clears_the_declaration_and_hides() {
|
||||
// The grid/LOCAL allocator, which mints its own epochs. `LOCAL` is
|
||||
// panel-capable, so this is the production path for a TUI.
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
state.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
|
||||
exec(
|
||||
|
|
@ -1340,3 +1340,10 @@ fn sweep_a_panel_wider_than_the_terminal_cap_still_presents_its_terminal() {
|
|||
);
|
||||
exec(&session.state, "pmacs.terminal.terminate(TERM_BUF)");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -366,3 +366,10 @@ fn toggle_between_kills_breaks_the_kill_chain() {
|
|||
"C-k, M-;, C-k yields two ring entries (chain broken)"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -159,9 +159,31 @@ pub fn spawn_daemon_process_with_env(socket_path: &Path, env_vars: &[(&str, &str
|
|||
cmd.args(["--daemon", "--socket"])
|
||||
.arg(socket_path)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::from(stderr));
|
||||
// All FIVE storage variables, not just `XDG_CONFIG_HOME`
|
||||
// (`docs/test-ambient-config-isolation-framing.md` §1.6a).
|
||||
//
|
||||
// `HOME` above is only a *fallback*: it isolates a root only while
|
||||
// the corresponding `XDG_*` variable is unset. On a developer
|
||||
// machine that exports `XDG_DATA_HOME`, a daemon given `HOME` alone
|
||||
// still materializes bundled packages into the real data root — so
|
||||
// the harness's apparent adequacy was a property of one machine's
|
||||
// environment, not of the harness. And `PMACS_STATE_HOME` outranks
|
||||
// `XDG_STATE_HOME`, so the four XDG variables alone leave a
|
||||
// higher-precedence state override live.
|
||||
//
|
||||
// Flat layout (every root at the socket's tempdir) so
|
||||
// `spawn_with_config`'s `<tempdir>/pmacs/init.lua` keeps loading
|
||||
// through the real `load_user_config` path.
|
||||
let roots = pmacs::bootstrap::BootstrapRoots::ambient()
|
||||
.with_config_root(isolated_home.to_path_buf())
|
||||
.with_data_root(isolated_home.to_path_buf())
|
||||
.with_state_root(isolated_home.to_path_buf())
|
||||
.with_cache_root(isolated_home.to_path_buf());
|
||||
for (key, value) in roots.child_env() {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
for (key, value) in env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -112,6 +112,21 @@ pub fn spawn_pmacs_in_pty(args: &[&str], envs: &[(&str, &Path)], rows: u16, cols
|
|||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
// Backfill any of the five storage variables the caller did not set
|
||||
// (`docs/test-ambient-config-isolation-framing.md` §1.6a). Callers
|
||||
// pin the roots their assertions read (a per-test `XDG_CONFIG_HOME`,
|
||||
// a `PMACS_STATE_HOME` they inspect); the rest would otherwise be
|
||||
// inherited, and the child would materialize bundled packages into
|
||||
// the developer's real data root. Caller entries win: this only
|
||||
// fills holes.
|
||||
let fallback = pmacs::bootstrap::BootstrapRoots::isolated_under(&super::iso::base());
|
||||
for (key, value) in fallback.child_env() {
|
||||
if envs.iter().any(|(k, _)| *k == key) {
|
||||
continue;
|
||||
}
|
||||
std::fs::create_dir_all(&value).expect("create isolated root for PTY child");
|
||||
cmd.env(key, value.as_os_str());
|
||||
}
|
||||
for (k, v) in envs {
|
||||
let mut value = OsString::new();
|
||||
value.push(v);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ fn errors_buffer(s: &EditorState) -> String {
|
|||
/// Fresh editor with LSP spawning disabled (language detection still
|
||||
/// works; the after-load hook must not exec real servers).
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s
|
||||
}
|
||||
|
|
@ -2853,3 +2853,10 @@ fn j1b1_context_reports_the_cwd_a_run_would_use() {
|
|||
);
|
||||
assert_eq!(kind, "rust", "the kind is detected from the given cwd");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fn probe(s: &EditorState) -> (String, bool, i64) {
|
|||
/// the prefix is replaced by the candidate in one step.
|
||||
#[test]
|
||||
fn typing_opens_popup_and_tab_accepts() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world ");
|
||||
let (_, visible, _) = probe(&s);
|
||||
assert!(
|
||||
|
|
@ -95,7 +95,7 @@ fn typing_opens_popup_and_tab_accepts() {
|
|||
/// candidate wins. Uses a custom provider for a deterministic order.
|
||||
#[test]
|
||||
fn ctrl_n_navigates_then_ret_accepts_second_candidate() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
|
|
@ -133,7 +133,7 @@ fn ctrl_n_navigates_then_ret_accepts_second_candidate() {
|
|||
/// key self-inserts normally (the shadow is partial, not modal).
|
||||
#[test]
|
||||
fn esc_dismisses_and_typing_falls_through() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world he");
|
||||
let (_, visible, _) = probe(&s);
|
||||
assert!(visible);
|
||||
|
|
@ -154,7 +154,7 @@ fn esc_dismisses_and_typing_falls_through() {
|
|||
/// anchor.
|
||||
#[test]
|
||||
fn motion_before_anchor_closes_popup() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world he");
|
||||
let (_, visible, _) = probe(&s);
|
||||
assert!(visible);
|
||||
|
|
@ -169,7 +169,7 @@ fn motion_before_anchor_closes_popup() {
|
|||
/// the Q#C9 single-char signature rejects paste-shaped deltas.
|
||||
#[test]
|
||||
fn yank_shaped_edit_does_not_auto_open() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world hello");
|
||||
// Select the trailing word and cut it (C-w): the popup that was
|
||||
// open over `hello` closes as its word dies.
|
||||
|
|
@ -198,7 +198,7 @@ fn yank_shaped_edit_does_not_auto_open() {
|
|||
/// auto-open prefix threshold.
|
||||
#[test]
|
||||
fn at_point_command_opens_below_threshold() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world h");
|
||||
let (_, visible, _) = probe(&s);
|
||||
assert!(!visible, "a 1-char prefix stays below the auto-open bar");
|
||||
|
|
@ -223,7 +223,7 @@ fn at_point_command_opens_below_threshold() {
|
|||
/// `C-g` abort) reaches the dispatcher instead of the popup shadow.
|
||||
#[test]
|
||||
fn pending_prefix_dismisses_popup_and_keeps_dispatcher_control() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello_world he");
|
||||
let (_, visible, _) = probe(&s);
|
||||
assert!(visible);
|
||||
|
|
@ -258,7 +258,7 @@ fn pending_prefix_dismisses_popup_and_keeps_dispatcher_control() {
|
|||
/// index sweep meant the server was never asked).
|
||||
#[test]
|
||||
fn empty_sync_sweep_still_leaves_a_pending_session() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// A buffer whose only word is the one being typed: dabbrev is
|
||||
// structurally empty, no LSP attached. The popup cannot open...
|
||||
type_str(&mut s, "qz");
|
||||
|
|
@ -282,7 +282,7 @@ fn empty_sync_sweep_still_leaves_a_pending_session() {
|
|||
/// because the real LSP store is Rust-side.
|
||||
#[test]
|
||||
fn collect_scopes_lsp_candidates_to_ctx_uri() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let (scoped, unscoped): (u64, u64) = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -322,3 +322,10 @@ fn collect_scopes_lsp_candidates_to_ctx_uri() {
|
|||
assert_eq!(scoped, 1, "ctx.uri reaches Lua providers (9th arg)");
|
||||
assert_eq!(unscoped, 2, "no uri → all documents");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -507,3 +507,10 @@ fn describe_setting_shows_a_buffer_local_override_when_one_exists() {
|
|||
"an existing buffer-local override must be reported, got {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fn probe(s: &EditorState) -> (String, bool, i64) {
|
|||
|
||||
#[test]
|
||||
fn backspace_deletes_the_shift_selected_region() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello");
|
||||
|
||||
// Shift+Left three times: region [2, 5), cursor at 2.
|
||||
|
|
@ -85,7 +85,7 @@ fn backspace_deletes_the_shift_selected_region() {
|
|||
/// chorded deletion keys to this same dispatch path.
|
||||
#[test]
|
||||
fn ctrl_backspace_deletes_the_previous_word() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "alpha beta");
|
||||
|
||||
s.dispatch_key(
|
||||
|
|
@ -111,7 +111,7 @@ fn ctrl_backspace_deletes_the_previous_word() {
|
|||
|
||||
#[test]
|
||||
fn typing_replaces_the_shift_selected_region() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello");
|
||||
|
||||
// Select "llo" (region [2, 5), cursor at 2), then type 'X':
|
||||
|
|
@ -149,7 +149,7 @@ fn typing_replaces_the_shift_selected_region() {
|
|||
|
||||
#[test]
|
||||
fn type_over_is_a_single_undo_step() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello");
|
||||
|
||||
// Select "llo" (region [2, 5)) and type 'X' → "heX".
|
||||
|
|
@ -177,7 +177,7 @@ fn type_over_is_a_single_undo_step() {
|
|||
|
||||
#[test]
|
||||
fn delete_forward_deletes_the_shift_selected_region() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "world");
|
||||
|
||||
// Shift+Home-equivalent: extend left over the whole word.
|
||||
|
|
@ -200,3 +200,10 @@ fn delete_forward_deletes_the_shift_selected_region() {
|
|||
assert_eq!(text, "b", "no region ⇒ plain forward delete at cursor");
|
||||
assert_eq!(cursor, 0);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -428,3 +428,10 @@ fn startup_gate_respects_file_arg_and_arming() {
|
|||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
|
|||
/// frame size *is* its geometry declaration, and the panel tests need
|
||||
/// one before any side window can be placed).
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
|
||||
s
|
||||
|
|
@ -1997,3 +1997,10 @@ fn dired_the_fold_refusal_names_the_read_only_lock() {
|
|||
"and not the sentence that is no longer true; got {st:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ fn status(s: &EditorState) -> String {
|
|||
/// typing RET would route through edit.newline-and-indent and clone
|
||||
/// leading whitespace), cursor at 0.
|
||||
fn editor_with(text: &str) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
&format!(
|
||||
|
|
@ -1155,7 +1155,7 @@ fn temp_path(name: &str) -> std::path::PathBuf {
|
|||
fn trim_on_save_defaults_off_and_writes_untouched_bytes() {
|
||||
let path = temp_path("off.txt");
|
||||
std::fs::write(&path, "x \ny\t\n").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let on: bool = eval(&s, "return pmacs.editops.trim_on_save()");
|
||||
assert!(!on, "default off");
|
||||
exec(
|
||||
|
|
@ -1175,7 +1175,7 @@ fn trim_on_save_defaults_off_and_writes_untouched_bytes() {
|
|||
fn trim_on_save_trims_the_written_bytes_before_later_callbacks() {
|
||||
let path = temp_path("on.txt");
|
||||
std::fs::write(&path, "x \ny\t\n").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.editops.trim_on_save(true)");
|
||||
// A callback registered AFTER editops' (load-time) hook observes
|
||||
// the fan-out order saveplace sees: post-trim text.
|
||||
|
|
@ -1215,7 +1215,7 @@ fn trim_on_save_trims_the_written_bytes_before_later_callbacks() {
|
|||
fn trim_on_save_failure_never_vetoes_the_save() {
|
||||
let path = temp_path("veto-immune.txt");
|
||||
std::fs::write(&path, "x \n").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.editops.trim_on_save(true)");
|
||||
exec(
|
||||
&s,
|
||||
|
|
@ -1246,7 +1246,7 @@ fn trim_on_save_failure_never_vetoes_the_save() {
|
|||
fn trim_on_save_unexpected_error_reports_and_still_saves() {
|
||||
let path = temp_path("unexpected.txt");
|
||||
std::fs::write(&path, "x \n").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.editops.trim_on_save(true)");
|
||||
// Capture the pmacs.error log (the m9_6 stub pattern — the
|
||||
// `if pmacs.error` branch is a no-op without it).
|
||||
|
|
@ -1293,7 +1293,7 @@ fn trim_on_save_unexpected_error_reports_and_still_saves() {
|
|||
fn another_callbacks_veto_is_not_masked_by_trim() {
|
||||
let path = temp_path("veto.txt");
|
||||
std::fs::write(&path, "x \n").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.editops.trim_on_save(true)");
|
||||
exec(
|
||||
&s,
|
||||
|
|
@ -1318,3 +1318,10 @@ fn another_callbacks_veto_is_not_masked_by_trim() {
|
|||
);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ fn status(s: &EditorState) -> String {
|
|||
fn editor_in(dir: &std::path::Path) -> EditorState {
|
||||
let anchor = dir.join("anchor.txt");
|
||||
std::fs::write(&anchor, b"anchor\n").expect("write anchor");
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
let anchor_str = anchor.display().to_string();
|
||||
exec(
|
||||
|
|
@ -279,7 +279,7 @@ fn find_file_accepting_a_directory_reports_instead_of_raising() {
|
|||
/// stable, real candidate there.
|
||||
#[test]
|
||||
fn find_file_without_a_backing_path_roots_at_the_process_cwd() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.reopen_init_phase_for_testing();
|
||||
assert!(
|
||||
active_path(&s).is_none(),
|
||||
|
|
@ -352,7 +352,7 @@ fn find_file_expands_a_leading_tilde() {
|
|||
return;
|
||||
}
|
||||
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.reopen_init_phase_for_testing();
|
||||
open_prompt(&mut s);
|
||||
// Contains a '/', so the typed text reaches on_accept verbatim.
|
||||
|
|
@ -371,3 +371,10 @@ fn find_file_expands_a_leading_tilde() {
|
|||
"the expansion must use $HOME; got {path} with HOME={home}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ fn insert_into(s: &EditorState, id: BufferId, text: &str) {
|
|||
|
||||
#[test]
|
||||
fn command_path_self_insert_unfolds_at_point() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
insert_into(&s, id, "line0\nline1\nline2\nline3\n");
|
||||
// Fold the interior of lines 1..2: [end of line0, end of line2].
|
||||
|
|
@ -330,7 +330,7 @@ fn self_insert_at_head_line_end_does_not_unfold() {
|
|||
// `(start, end]` containment: a self-insert exactly at the end of the
|
||||
// head line (== range.start) is outside the fold — it must NOT unfold,
|
||||
// and the translator shifts the fold right so the char lands visible.
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
insert_into(&s, id, "line0\nline1\nline2\nline3\n");
|
||||
let store = {
|
||||
|
|
@ -389,7 +389,7 @@ fn install_rust_parse(s: &EditorState, id: BufferId) {
|
|||
|
||||
#[test]
|
||||
fn folding_moves_point_to_head_line() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n";
|
||||
insert_into(&s, id, src);
|
||||
|
|
@ -413,7 +413,7 @@ fn folding_moves_point_to_head_line() {
|
|||
|
||||
#[test]
|
||||
fn data_api_validation() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// A plain document buffer with four lines.
|
||||
exec(
|
||||
&s,
|
||||
|
|
@ -465,7 +465,7 @@ fn data_api_validation() {
|
|||
|
||||
#[test]
|
||||
fn forget_drops_store_and_detaches_view() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
insert_into(&s, id, "line0\nline1\nline2\n");
|
||||
let store = {
|
||||
|
|
@ -500,7 +500,7 @@ fn killing_a_buffer_through_the_real_path_purges_its_fold_store() {
|
|||
// assertion reads through the DEAD id on purpose: BufferIds are never
|
||||
// reused, so a stale id cannot alias a later buffer. Mirrors
|
||||
// config_registry's `killing_a_buffer_through_the_real_path_...`.
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"b = pmacs.buffer.from_bytes('kill.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
|
||||
|
|
@ -530,7 +530,7 @@ fn close_all_command_moves_point_to_enclosing_head() {
|
|||
// Q#FD3 through the command surface: `fold.close-all` is interactive,
|
||||
// so when it collapses a top-level fold around the invoking point, the
|
||||
// point moves to that fold's head line (Finding 3, round 1).
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
let src = "fn first() {\n a();\n b();\n}\nfn second() {\n c();\n d();\n}\n";
|
||||
insert_into(&s, id, src);
|
||||
|
|
@ -553,7 +553,7 @@ fn close_all_command_moves_point_to_enclosing_head() {
|
|||
fn stale_parse_tree_refuses_fold() {
|
||||
// Q#FD10: an edit after the parse leaves `pending_edit_count() > 0`,
|
||||
// so a fold command refuses (the settled coordinates are stale).
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id = active_id(&s);
|
||||
let src = "fn foo() {\n let x = 1;\n let y = 2;\n}\n";
|
||||
insert_into(&s, id, src);
|
||||
|
|
@ -571,7 +571,7 @@ fn stale_parse_tree_refuses_fold() {
|
|||
fn read_only_buffer_is_rejected() {
|
||||
// Q#FD11's "normal document buffer" guard: terminals are read-only, so
|
||||
// a read-only buffer is not foldable.
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"b = pmacs.buffer.from_bytes('ro.rs', 'aaa\\nbbb\\nccc\\n')",
|
||||
|
|
@ -592,7 +592,7 @@ fn read_only_buffer_is_rejected() {
|
|||
|
||||
#[test]
|
||||
fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
|
||||
|
|
@ -608,3 +608,10 @@ fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() {
|
|||
let n: i64 = eval(&s, "return #pmacs.fold.folds(b)");
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ fn end_of(line: usize) -> u64 {
|
|||
}
|
||||
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s
|
||||
}
|
||||
|
|
@ -1772,3 +1772,10 @@ fn wheel_over_an_inactive_unfolded_pane_uses_that_windows_map() {
|
|||
"the folded pane did not scroll"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ use pmacs::editor::EditorState;
|
|||
use pmacs::protocol::{ByteRange, FrontendId, InstanceMessage};
|
||||
use pmacs::semantic_render::SemanticRenderState;
|
||||
|
||||
#[cfg(feature = "crdt")]
|
||||
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
|
||||
// build, not only the CRDT one.
|
||||
mod common;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -43,7 +44,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
|
|||
|
||||
/// Fresh editor with LSP spawning disabled.
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s
|
||||
}
|
||||
|
|
@ -490,3 +491,11 @@ fn preference_set_before_attach_ships_on_the_first_frame() {
|
|||
"the first frame ships the pre-attach preference"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ fn non_crdt_root_rejects_gpu_before_socket_io_discovery_or_spawn() {
|
|||
.arg("--gpu")
|
||||
.env(TEST_GPU_OVERRIDE, &fake_gpu)
|
||||
.env("PMACS_TEST_MARKER", &marker)
|
||||
.env("XDG_CONFIG_HOME", temp.path())
|
||||
.env("XDG_DATA_HOME", temp.path())
|
||||
.env("XDG_STATE_HOME", temp.path())
|
||||
.env("PMACS_STATE_HOME", temp.path())
|
||||
.env("XDG_CACHE_HOME", temp.path())
|
||||
.env("XDG_RUNTIME_DIR", &runtime)
|
||||
.output()
|
||||
.expect("run non-CRDT pmacs --gpu");
|
||||
|
|
@ -61,6 +66,11 @@ fn non_crdt_root_rejects_gpu_before_socket_io_discovery_or_spawn() {
|
|||
.arg(&occupied_socket)
|
||||
.env(TEST_GPU_OVERRIDE, &fake_gpu)
|
||||
.env("PMACS_TEST_MARKER", &marker)
|
||||
.env("XDG_CONFIG_HOME", temp.path())
|
||||
.env("XDG_DATA_HOME", temp.path())
|
||||
.env("XDG_STATE_HOME", temp.path())
|
||||
.env("PMACS_STATE_HOME", temp.path())
|
||||
.env("XDG_CACHE_HOME", temp.path())
|
||||
.output()
|
||||
.expect("run non-CRDT pmacs --gpu against occupied socket");
|
||||
assert!(!occupied.status.success());
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fn pump_async<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
|
|||
/// resolver would leave the Lua write-through + snapshot bridge unproven.
|
||||
#[test]
|
||||
fn lua_alias_override_resolves_on_async_parse() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
|
||||
// Add a bespoke fence alias from Lua (write-through to the registry).
|
||||
state
|
||||
|
|
@ -98,7 +98,7 @@ fn lua_alias_override_resolves_on_async_parse() {
|
|||
/// alias discriminates the fix: with the empty map it would not resolve.
|
||||
#[test]
|
||||
fn sync_parse_now_resolves_alias() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let src = b"# Doc\n\n```py\nx = 1\n```\n";
|
||||
let buf_id = state
|
||||
.lua_host
|
||||
|
|
@ -136,7 +136,7 @@ fn sync_parse_now_resolves_alias() {
|
|||
/// the file drops below the cap and exceeds it again.
|
||||
#[test]
|
||||
fn injection_cap_surfaced_once_and_rearms_via_lua() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Capture pmacs.error messages into a Lua global.
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -305,3 +305,10 @@ fn many_paragraph_settle_under_budget_with_tail_covered() {
|
|||
"the final paragraph still receives an inline layer (no tail loss)"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -209,6 +209,85 @@ fn launch(path: &Path) -> EditorState {
|
|||
s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ambient-roots isolation
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// This suite is the one place that must NOT take the
|
||||
// `EditorState::new_with_roots` / `open_with_roots` seam
|
||||
// (`docs/test-ambient-config-isolation-framing.md` §1.7). Its whole job
|
||||
// is to prove the production entry point is wired: an `open` arm with no
|
||||
// production caller passes every direct-call test, so the ratchet drives
|
||||
// the same `EditorState::open` that `pmacs FILE` does — parameterless,
|
||||
// ambient, exactly as production calls it.
|
||||
//
|
||||
// That leaves nothing in the process able to isolate it. Cargo launches
|
||||
// each integration-test binary with the caller's environment, and a
|
||||
// binary cannot re-point its own roots before its tests run:
|
||||
// `std::env::set_var` is `unsafe` and pmacs is
|
||||
// `#![forbid(unsafe_code)]`. "Isolated by the environment it is launched
|
||||
// with" is not a mechanism — it is a hope that whoever typed `cargo
|
||||
// test` wrapped it, which is the external workaround this lane exists to
|
||||
// delete.
|
||||
//
|
||||
// So each test is a thin **parent** that re-execs this same binary for
|
||||
// its own test name, with controlled roots in the child's environment.
|
||||
// The child sees the marker, runs the real body, and calls the ambient
|
||||
// entry point — production's call, against a controlled tree.
|
||||
|
||||
/// Marker the parent sets on the child. Its presence, not its value, is
|
||||
/// the signal.
|
||||
const ISOLATED_CHILD: &str = "PMACS_JOURNEY_ISOLATED_CHILD";
|
||||
|
||||
/// Re-exec this test binary for `name` under controlled bootstrap
|
||||
/// storage roots.
|
||||
///
|
||||
/// Returns `true` in the **parent** — the child has already run and been
|
||||
/// asserted, so the caller must return without doing anything itself —
|
||||
/// and `false` in the **child**, whose job is to run the body.
|
||||
#[must_use]
|
||||
fn reexec_isolated(name: &str) -> bool {
|
||||
if std::env::var_os(ISOLATED_CHILD).is_some() {
|
||||
return false;
|
||||
}
|
||||
// Under `target/`, not `/tmp`: nothing unlinks this (libtest has no
|
||||
// teardown hook), so it belongs somewhere `cargo clean` owns.
|
||||
let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
|
||||
.join("journey-isolation")
|
||||
.join(name);
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
let roots = pmacs::bootstrap::BootstrapRoots::isolated_under(&base);
|
||||
let child_env = roots.child_env();
|
||||
for (_, dir) in &child_env {
|
||||
std::fs::create_dir_all(dir).expect("create controlled root");
|
||||
}
|
||||
let exe = std::env::current_exe().expect("current test binary");
|
||||
let output = std::process::Command::new(exe)
|
||||
.args(["--exact", name, "--nocapture", "--test-threads=1"])
|
||||
.env(ISOLATED_CHILD, "1")
|
||||
.envs(child_env)
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("re-exec `{name}`: {e}"));
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"isolated child for `{name}` failed ({})\n--- child stdout ---\n{stdout}\
|
||||
\n--- child stderr ---\n{stderr}",
|
||||
output.status
|
||||
);
|
||||
// A filter that matches nothing exits 0. Without this the parent
|
||||
// would pass while running no test at all — the failure mode a
|
||||
// renamed test produces, and the one that would quietly hollow out
|
||||
// the whole ratchet.
|
||||
assert!(
|
||||
stdout.contains("1 passed"),
|
||||
"isolated child for `{name}` ran no test — the `--exact` filter went \
|
||||
stale against the function name\n--- child stdout ---\n{stdout}"
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 2 — launch unconfigured
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -216,6 +295,9 @@ fn launch(path: &Path) -> EditorState {
|
|||
/// **N** — the editor starts with no configuration and no arguments.
|
||||
#[test]
|
||||
fn journey_step2_launches_unconfigured_into_scratch() {
|
||||
if reexec_isolated("journey_step2_launches_unconfigured_into_scratch") {
|
||||
return;
|
||||
}
|
||||
let s = EditorState::new();
|
||||
assert_eq!(active_name(&s), "*scratch*");
|
||||
assert!(
|
||||
|
|
@ -236,6 +318,9 @@ fn journey_step2_launches_unconfigured_into_scratch() {
|
|||
/// `Err(EISDIR)` and `main` exited 1.
|
||||
#[test]
|
||||
fn journey_step3_opening_a_directory_lists_it() {
|
||||
if reexec_isolated("journey_step3_opening_a_directory_lists_it") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let s = launch(td.path());
|
||||
|
||||
|
|
@ -259,6 +344,9 @@ fn journey_step3_opening_a_directory_lists_it() {
|
|||
/// the assertion above while `pmacs .` still printed a diagnostic.
|
||||
#[test]
|
||||
fn journey_step3_directory_startup_reports_no_error() {
|
||||
if reexec_isolated("journey_step3_directory_startup_reports_no_error") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let s = launch(td.path());
|
||||
assert!(
|
||||
|
|
@ -274,6 +362,9 @@ fn journey_step3_directory_startup_reports_no_error() {
|
|||
#[test]
|
||||
fn journey_step3_unreadable_directory_reports_without_failing_startup() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if reexec_isolated("journey_step3_unreadable_directory_reports_without_failing_startup") {
|
||||
return;
|
||||
}
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let locked = td.path().join("locked");
|
||||
std::fs::create_dir(&locked).expect("mkdir");
|
||||
|
|
@ -301,6 +392,9 @@ fn journey_step3_unreadable_directory_reports_without_failing_startup() {
|
|||
/// because no buffer is created and `set_buffer_path` never runs.
|
||||
#[test]
|
||||
fn journey_directory_resolver_receives_a_canonical_path() {
|
||||
if reexec_isolated("journey_directory_resolver_receives_a_canonical_path") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -331,6 +425,9 @@ fn journey_directory_resolver_receives_a_canonical_path() {
|
|||
/// fallback is a clearable slot rather than a builtin hook subscription.
|
||||
#[test]
|
||||
fn journey_unclaimed_directory_starts_successfully_with_a_status() {
|
||||
if reexec_isolated("journey_unclaimed_directory_starts_successfully_with_a_status") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -360,6 +457,9 @@ fn journey_unclaimed_directory_starts_successfully_with_a_status() {
|
|||
/// a claim suppresses the fallback.
|
||||
#[test]
|
||||
fn journey_resolver_chain_is_first_claimant_wins() {
|
||||
if reexec_isolated("journey_resolver_chain_is_first_claimant_wins") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -395,6 +495,9 @@ fn journey_resolver_chain_is_first_claimant_wins() {
|
|||
/// `proceed == false`.
|
||||
#[test]
|
||||
fn journey_a_raising_resolver_suppresses_the_fallback_and_reports() {
|
||||
if reexec_isolated("journey_a_raising_resolver_suppresses_the_fallback_and_reports") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -458,6 +561,9 @@ const COMPETITOR: FrontendId = FrontendId(7);
|
|||
/// ambient display: the file then appears in the competitor's window.
|
||||
#[test]
|
||||
fn commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one() {
|
||||
if reexec_isolated("commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -519,6 +625,9 @@ fn commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one() {
|
|||
/// `acting_frontend`, or by reordering it after the interactive origin.
|
||||
#[test]
|
||||
fn commit_to_outranks_an_interactive_origin() {
|
||||
if reexec_isolated("commit_to_outranks_an_interactive_origin") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -586,6 +695,9 @@ fn commit_to_outranks_an_interactive_origin() {
|
|||
/// `display`, or by reading `prev` from the ambient window.
|
||||
#[test]
|
||||
fn a_background_open_uses_the_captured_window_not_the_selected_one() {
|
||||
if reexec_isolated("a_background_open_uses_the_captured_window_not_the_selected_one") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -652,6 +764,9 @@ fn a_background_open_uses_the_captured_window_not_the_selected_one() {
|
|||
/// competitor and the assertion fails from the other direction).
|
||||
#[test]
|
||||
fn commit_to_scopes_and_restores_on_a_normal_return() {
|
||||
if reexec_isolated("commit_to_scopes_and_restores_on_a_normal_return") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -701,6 +816,9 @@ fn commit_to_scopes_and_restores_on_a_normal_return() {
|
|||
/// through the scope, or by restoring on the success path only.
|
||||
#[test]
|
||||
fn commit_to_restores_when_the_callback_raises() {
|
||||
if reexec_isolated("commit_to_restores_when_the_callback_raises") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -744,6 +862,9 @@ fn commit_to_restores_when_the_callback_raises() {
|
|||
/// `<no raise>`.
|
||||
#[test]
|
||||
fn commit_to_refuses_an_await_and_restores() {
|
||||
if reexec_isolated("commit_to_refuses_an_await_and_restores") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -795,6 +916,9 @@ fn commit_to_refuses_an_await_and_restores() {
|
|||
/// userdata after invoking the callback.
|
||||
#[test]
|
||||
fn commit_to_refuses_a_forged_destination() {
|
||||
if reexec_isolated("commit_to_refuses_a_forged_destination") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -831,6 +955,9 @@ fn commit_to_refuses_a_forged_destination() {
|
|||
/// a shared mutable table.
|
||||
#[test]
|
||||
fn a_declining_listener_cannot_redirect_the_destination() {
|
||||
if reexec_isolated("a_declining_listener_cannot_redirect_the_destination") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -907,6 +1034,9 @@ fn a_declining_listener_cannot_redirect_the_destination() {
|
|||
/// distinguish "validates" from "validates in time".
|
||||
#[test]
|
||||
fn preservation_a_failed_precondition_never_reaches_the_callback() {
|
||||
if reexec_isolated("preservation_a_failed_precondition_never_reaches_the_callback") {
|
||||
return;
|
||||
}
|
||||
// (label, Lua that breaks the precondition, expected reason fragment)
|
||||
let cases: [(&str, &str, &str); 4] = [
|
||||
(
|
||||
|
|
@ -994,6 +1124,9 @@ fn preservation_a_failed_precondition_never_reaches_the_callback() {
|
|||
/// (window-only validation). The dired buffer then replaces the user's.
|
||||
#[test]
|
||||
fn preservation_a_stale_destination_loses_to_the_users_newer_buffer() {
|
||||
if reexec_isolated("preservation_a_stale_destination_loses_to_the_users_newer_buffer") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -1062,6 +1195,11 @@ fn preservation_a_stale_destination_loses_to_the_users_newer_buffer() {
|
|||
/// `*competitor*`.
|
||||
#[test]
|
||||
fn preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend() {
|
||||
if reexec_isolated(
|
||||
"preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -1117,6 +1255,9 @@ fn preservation_dired_captures_prev_from_the_destination_not_the_ambient_fronten
|
|||
/// the read-only contract rather than pin the journey.
|
||||
#[test]
|
||||
fn journey_step5_editing_a_file_reached_through_the_directory() {
|
||||
if reexec_isolated("journey_step5_editing_a_file_reached_through_the_directory") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = launch(td.path());
|
||||
assert!(active_name(&s).starts_with("*dired:"));
|
||||
|
|
@ -1169,6 +1310,9 @@ fn journey_step5_editing_a_file_reached_through_the_directory() {
|
|||
/// preserve is which window shows the file, and that is what this pins.
|
||||
#[test]
|
||||
fn preservation_opening_a_file_shows_it_in_the_active_window() {
|
||||
if reexec_isolated("preservation_opening_a_file_shows_it_in_the_active_window") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let target = td.path().join("alpha.txt");
|
||||
let s = EditorState::open(target.clone()).expect("open");
|
||||
|
|
@ -1204,6 +1348,9 @@ fn preservation_opening_a_file_shows_it_in_the_active_window() {
|
|||
/// failure mode is a hard error on a perfectly ordinary gesture.
|
||||
#[test]
|
||||
fn preservation_a_missing_path_becomes_a_new_file_buffer() {
|
||||
if reexec_isolated("preservation_a_missing_path_becomes_a_new_file_buffer") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let fresh = td.path().join("not-yet.txt");
|
||||
let s = EditorState::open(fresh.clone()).expect("a missing path is not an error");
|
||||
|
|
@ -1223,6 +1370,9 @@ fn preservation_a_missing_path_becomes_a_new_file_buffer() {
|
|||
#[test]
|
||||
fn preservation_an_unreadable_file_reports_with_its_path() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if reexec_isolated("preservation_an_unreadable_file_reports_with_its_path") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let locked = td.path().join("locked.txt");
|
||||
std::fs::write(&locked, b"secret\n").expect("write");
|
||||
|
|
@ -1270,6 +1420,9 @@ fn preservation_an_unreadable_file_reports_with_its_path() {
|
|||
/// real accept path; this one pins the primitive and the window state.
|
||||
#[test]
|
||||
fn preservation_display_file_still_refuses_a_directory() {
|
||||
if reexec_isolated("preservation_display_file_still_refuses_a_directory") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::open(td.path().join("alpha.txt")).expect("open");
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -1440,6 +1593,9 @@ fn walk_to_open_file(dir: &Path, name: &str) -> EditorState {
|
|||
/// `COHERENCE.md` says is missing.
|
||||
#[test]
|
||||
fn journey_step9_the_compile_chord_opens_the_prompt() {
|
||||
if reexec_isolated("journey_step9_the_compile_chord_opens_the_prompt") {
|
||||
return;
|
||||
}
|
||||
let td = cargo_project();
|
||||
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||
press_compile_chord(&mut s);
|
||||
|
|
@ -1453,6 +1609,9 @@ fn journey_step9_the_compile_chord_opens_the_prompt() {
|
|||
/// **N** — the prompt is prefilled from the detected project kind.
|
||||
#[test]
|
||||
fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
|
||||
if reexec_isolated("journey_step9_the_prompt_is_prefilled_for_a_cargo_project") {
|
||||
return;
|
||||
}
|
||||
let td = cargo_project();
|
||||
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||
press_compile_chord(&mut s);
|
||||
|
|
@ -1472,6 +1631,9 @@ fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
|
|||
/// which is what re-resolving at accept time looks like.
|
||||
#[test]
|
||||
fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
|
||||
if reexec_isolated("journey_step9_the_prompt_runs_in_the_directory_it_captured") {
|
||||
return;
|
||||
}
|
||||
let a = cargo_project();
|
||||
let b = project();
|
||||
let mut s = walk_to_open_file(a.path(), "Cargo.toml");
|
||||
|
|
@ -1521,6 +1683,9 @@ fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
|
|||
/// real process.
|
||||
#[test]
|
||||
fn journey_step9_the_offered_command_builds_the_project() {
|
||||
if reexec_isolated("journey_step9_the_offered_command_builds_the_project") {
|
||||
return;
|
||||
}
|
||||
if !binary_available("cargo") {
|
||||
assert!(
|
||||
std::env::var_os("PMACS_REQUIRE_CARGO_BUILD").is_none(),
|
||||
|
|
@ -1574,6 +1739,9 @@ fn journey_step9_the_offered_command_builds_the_project() {
|
|||
/// from it yields `node` again and this pin stays green.
|
||||
#[test]
|
||||
fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
|
||||
if reexec_isolated("journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one") {
|
||||
return;
|
||||
}
|
||||
let outer = cargo_project();
|
||||
let sub = outer.path().join("sub");
|
||||
std::fs::create_dir_all(&sub).expect("mkdir sub");
|
||||
|
|
@ -1610,6 +1778,9 @@ fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
|
|||
/// runner's cwd and pinning it would pin the environment.
|
||||
#[test]
|
||||
fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
|
||||
if reexec_isolated("journey_step9_the_compile_context_is_total_even_with_no_file_open") {
|
||||
return;
|
||||
}
|
||||
let td = cargo_project();
|
||||
let s = launch(td.path());
|
||||
assert!(
|
||||
|
|
@ -1638,6 +1809,9 @@ fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
|
|||
/// is reordering the precedence chain to put `defaults[kind]` first.
|
||||
#[test]
|
||||
fn journey_step9_preservation_the_last_command_outranks_the_default() {
|
||||
if reexec_isolated("journey_step9_preservation_the_last_command_outranks_the_default") {
|
||||
return;
|
||||
}
|
||||
let td = cargo_project();
|
||||
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||
exec(&s, "pmacs.compile.run('true')");
|
||||
|
|
@ -1660,6 +1834,9 @@ fn journey_step9_preservation_the_last_command_outranks_the_default() {
|
|||
/// over one of these sequences.
|
||||
#[test]
|
||||
fn journey_step9_preservation_the_existing_compile_bindings_survive() {
|
||||
if reexec_isolated("journey_step9_preservation_the_existing_compile_bindings_survive") {
|
||||
return;
|
||||
}
|
||||
let td = cargo_project();
|
||||
let s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||
for (sequence, command) in [
|
||||
|
|
@ -1705,6 +1882,9 @@ fn binary_available(name: &str) -> bool {
|
|||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn journey_step9_the_compile_directory_is_detection_canonical() {
|
||||
if reexec_isolated("journey_step9_the_compile_directory_is_detection_canonical") {
|
||||
return;
|
||||
}
|
||||
let parent = tempfile::tempdir().expect("tempdir");
|
||||
let real = parent.path().join("real");
|
||||
std::fs::create_dir_all(real.join("src")).expect("mkdir real");
|
||||
|
|
@ -1804,6 +1984,9 @@ fn start_local() -> EditorState {
|
|||
/// `prepare_startup` — the mutation every by-hand pin would survive.
|
||||
#[test]
|
||||
fn journey_step4_a_no_target_launch_greets_in_scratch() {
|
||||
if reexec_isolated("journey_step4_a_no_target_launch_greets_in_scratch") {
|
||||
return;
|
||||
}
|
||||
let s = start_local();
|
||||
|
||||
// Preconditions asserted, not assumed (framing §3.2b): a developer
|
||||
|
|
@ -1835,6 +2018,9 @@ fn journey_step4_a_no_target_launch_greets_in_scratch() {
|
|||
/// is two chords and nothing in the rendered text marks the boundary.
|
||||
#[test]
|
||||
fn journey_step4_every_advertised_key_is_bound() {
|
||||
if reexec_isolated("journey_step4_every_advertised_key_is_bound") {
|
||||
return;
|
||||
}
|
||||
let s = start_local();
|
||||
let entries = welcome_entries(&s);
|
||||
assert!(
|
||||
|
|
@ -1858,6 +2044,9 @@ fn journey_step4_every_advertised_key_is_bound() {
|
|||
/// Pin 2 alone would pass if rendering silently dropped one.
|
||||
#[test]
|
||||
fn journey_step4_the_rendered_welcome_contains_every_entry() {
|
||||
if reexec_isolated("journey_step4_the_rendered_welcome_contains_every_entry") {
|
||||
return;
|
||||
}
|
||||
let s = start_local();
|
||||
let text = active_text(&s);
|
||||
for (keys, label) in welcome_entries(&s) {
|
||||
|
|
@ -1884,6 +2073,9 @@ fn journey_step4_the_rendered_welcome_contains_every_entry() {
|
|||
/// so nothing about the accepted value survives afterwards.
|
||||
#[test]
|
||||
fn journey_step4_m_x_help_renders_the_cheat_sheet() {
|
||||
if reexec_isolated("journey_step4_m_x_help_renders_the_cheat_sheet") {
|
||||
return;
|
||||
}
|
||||
let mut s = start_local();
|
||||
|
||||
s.dispatch_key(
|
||||
|
|
@ -1922,6 +2114,9 @@ fn journey_step4_m_x_help_renders_the_cheat_sheet() {
|
|||
/// would lift read-only, discard history, and fail the insert.
|
||||
#[test]
|
||||
fn journey_step4_preservation_the_greeted_scratch_is_editable_and_clean() {
|
||||
if reexec_isolated("journey_step4_preservation_the_greeted_scratch_is_editable_and_clean") {
|
||||
return;
|
||||
}
|
||||
let mut s = start_local();
|
||||
assert!(
|
||||
eval::<bool>(
|
||||
|
|
@ -1942,6 +2137,9 @@ fn journey_step4_preservation_the_greeted_scratch_is_editable_and_clean() {
|
|||
/// **P** — a file target does not greet.
|
||||
#[test]
|
||||
fn journey_step4_preservation_a_file_target_does_not_greet() {
|
||||
if reexec_isolated("journey_step4_preservation_a_file_target_does_not_greet") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let path = td.path().join("alpha.txt");
|
||||
let s = match pmacs::editor::prepare_startup(Some(path.clone())).expect("startup") {
|
||||
|
|
@ -1965,6 +2163,9 @@ fn journey_step4_preservation_a_file_target_does_not_greet() {
|
|||
/// still exists to be wrongly greeted.
|
||||
#[test]
|
||||
fn journey_step4_preservation_a_directory_target_does_not_greet() {
|
||||
if reexec_isolated("journey_step4_preservation_a_directory_target_does_not_greet") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s =
|
||||
match pmacs::editor::prepare_startup(Some(td.path().to_path_buf())).expect("startup") {
|
||||
|
|
@ -1979,6 +2180,9 @@ fn journey_step4_preservation_a_directory_target_does_not_greet() {
|
|||
/// **P** — a non-empty `*scratch*` is never overwritten.
|
||||
#[test]
|
||||
fn journey_step4_preservation_existing_scratch_content_survives() {
|
||||
if reexec_isolated("journey_step4_preservation_existing_scratch_content_survives") {
|
||||
return;
|
||||
}
|
||||
let mut s = EditorState::new();
|
||||
exec(&s, "pmacs.window.buffer():insert(0, 'user content')");
|
||||
s.finalize_local_launch(false);
|
||||
|
|
@ -1993,6 +2197,9 @@ fn journey_step4_preservation_existing_scratch_content_survives() {
|
|||
/// desktop restore having put something else in front.
|
||||
#[test]
|
||||
fn journey_step4_preservation_a_backgrounded_scratch_is_not_greeted() {
|
||||
if reexec_isolated("journey_step4_preservation_a_backgrounded_scratch_is_not_greeted") {
|
||||
return;
|
||||
}
|
||||
let td = project();
|
||||
let mut s = EditorState::new();
|
||||
exec(
|
||||
|
|
@ -2019,6 +2226,9 @@ fn journey_step4_preservation_a_backgrounded_scratch_is_not_greeted() {
|
|||
/// reached in production.
|
||||
#[test]
|
||||
fn journey_step4_preservation_constructors_never_greet() {
|
||||
if reexec_isolated("journey_step4_preservation_constructors_never_greet") {
|
||||
return;
|
||||
}
|
||||
let bare = EditorState::new();
|
||||
assert_eq!(scratch_text(&bare), "", "EditorState::new must not greet");
|
||||
|
||||
|
|
@ -2078,6 +2288,9 @@ fn lsp_segment(s: &EditorState) -> Option<String> {
|
|||
/// vacuous.
|
||||
#[test]
|
||||
fn journey_step6_a_missing_language_server_is_reported_not_swallowed() {
|
||||
if reexec_isolated("journey_step6_a_missing_language_server_is_reported_not_swallowed") {
|
||||
return;
|
||||
}
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(td.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").expect("write toml");
|
||||
std::fs::write(td.path().join("main.rs"), b"fn main() {}\n").expect("write rs");
|
||||
|
|
@ -2138,6 +2351,9 @@ fn journey_step6_a_missing_language_server_is_reported_not_swallowed() {
|
|||
#[test]
|
||||
fn journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame() {
|
||||
use pmacs::cell::{Cell, CellGrid, CellSize, Glyph};
|
||||
if reexec_isolated("journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame") {
|
||||
return;
|
||||
}
|
||||
|
||||
let s = start_local();
|
||||
let (rows, cols) = (12u32, 100u32);
|
||||
|
|
@ -2192,3 +2408,86 @@ fn journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame() {
|
|||
row_text(0)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The isolation mechanism itself
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// **N** — the re-exec of §1.10 actually redirects the child's roots.
|
||||
///
|
||||
/// Every other test in this file is *protected* by the mechanism; none
|
||||
/// of them fails if it silently stops working, because they assert about
|
||||
/// editor behaviour and a developer's `init.lua` usually leaves that
|
||||
/// alone. This one asserts the mechanism's own output: the roots the
|
||||
/// child resolves, from inside the child.
|
||||
///
|
||||
/// Falsified by dropping `.envs(child_env)` from `reexec_isolated` — the
|
||||
/// child then resolves the developer's real `~/.config/pmacs` and
|
||||
/// `~/.local/share`, and all four assertions below fail.
|
||||
#[test]
|
||||
fn journey_isolated_child_resolves_the_controlled_roots() {
|
||||
if reexec_isolated("journey_isolated_child_resolves_the_controlled_roots") {
|
||||
return;
|
||||
}
|
||||
let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
|
||||
.join("journey-isolation")
|
||||
.join("journey_isolated_child_resolves_the_controlled_roots");
|
||||
let under = |p: Option<std::path::PathBuf>, what: &str| {
|
||||
let p = p.unwrap_or_else(|| panic!("{what} must resolve in the child"));
|
||||
assert!(
|
||||
p.starts_with(&base),
|
||||
"{what} resolved to {} — outside the controlled base {}",
|
||||
p.display(),
|
||||
base.display()
|
||||
);
|
||||
};
|
||||
under(pmacs::config::user_config_dir(), "the config dir");
|
||||
under(
|
||||
Some(pmacs::builtin_packages::bundled_runtime_dir()),
|
||||
"the bundled-package dir",
|
||||
);
|
||||
// `PMACS_STATE_HOME` is the fifth variable: it outranks
|
||||
// `XDG_STATE_HOME`, so a child given only the four XDG ones inherits
|
||||
// whatever the launching shell exported.
|
||||
under(pmacs::state::user_state_dir(), "the state dir");
|
||||
under(pmacs::minibuffer::user_history_dir(), "the history dir");
|
||||
}
|
||||
|
||||
/// **N** — the ambient entry point is still what this suite drives.
|
||||
///
|
||||
/// The isolation seam has a `new_with_roots` / `open_with_roots` sibling
|
||||
/// that every other suite in the tree now takes. Taking it *here* would
|
||||
/// be a silent regression of what the ratchet exists to prove — a
|
||||
/// production entry point with no caller — and would leave every
|
||||
/// assertion in this file green. Asserted against this file's own source
|
||||
/// rather than left as an obvious-by-inspection property.
|
||||
#[test]
|
||||
fn journey_drives_the_ambient_production_entry_point() {
|
||||
let src = std::fs::read_to_string(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("journey_acceptance.rs"),
|
||||
)
|
||||
.expect("read this suite's own source");
|
||||
// Every needle is assembled rather than written whole: a literal
|
||||
// spelled out here would appear in this file and match itself, which
|
||||
// turns the positive check vacuous and the negative ones into
|
||||
// guaranteed failures.
|
||||
let ambient_open = concat!("EditorState::open(", "path.to_path_buf())");
|
||||
let seam_open = concat!("EditorState::open", "_with_roots(");
|
||||
let seam_new = concat!("EditorState::new", "_with_roots(");
|
||||
assert!(
|
||||
src.contains(ambient_open),
|
||||
"journey must call the parameterless production `open`"
|
||||
);
|
||||
assert!(
|
||||
!src.contains(seam_open),
|
||||
"journey must not take the isolation seam — its isolation comes \
|
||||
from the environment its child is launched with"
|
||||
);
|
||||
assert!(
|
||||
!src.contains(seam_new),
|
||||
"journey must not take the isolation seam — its isolation comes \
|
||||
from the environment its child is launched with"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ fn status(s: &EditorState) -> String {
|
|||
|
||||
/// Fresh editor whose scratch buffer holds `text`, cursor at 0.
|
||||
fn editor_with(text: &str) -> EditorState {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, text);
|
||||
exec(&s, "pmacs.editor.goto_byte(0)");
|
||||
s
|
||||
|
|
@ -855,3 +855,10 @@ fn frontend_detached_drops_per_frontend_state() {
|
|||
);
|
||||
assert!(gone, "detach dropped B's killring state");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ impl Fixture {
|
|||
/// resolver. That combination is the point: the root rule under test is
|
||||
/// production code, only the command is a stand-in.
|
||||
fn editor(fx: &Fixture) -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -1880,3 +1880,10 @@ fn r6_no_swap_retires_only_the_failed_root() {
|
|||
swap occurred"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ fn fresh_state_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn editor(state_dir: &std::path::Path) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -243,7 +243,7 @@ fn acc10b_the_prime_suffix_does_not_pair_in_lean() {
|
|||
/// WRITE-ONLY proxy (the canonical map lives Rust-side), so an
|
||||
/// alias-table read would prove nothing about what the parser does.
|
||||
fn markdown_layer_languages(src: &[u8]) -> Vec<String> {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let buf_id = state
|
||||
.lua_host
|
||||
.registry()
|
||||
|
|
@ -323,7 +323,7 @@ fn acc12_opening_lean_spawns_no_process_without_a_server_config() {
|
|||
|
||||
// Constructing an editor touches no process, even though the Lean
|
||||
// config now exists and names `lake`.
|
||||
let pristine = EditorState::new();
|
||||
let pristine = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let at_init: i64 = eval(&pristine, "return #pmacs.process.list()");
|
||||
assert_eq!(
|
||||
at_init, 0,
|
||||
|
|
@ -352,3 +352,10 @@ fn acc12_opening_lean_spawns_no_process_without_a_server_config() {
|
|||
"with no server configured, opening a Lean buffer spawns nothing"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ fn lean_editor() -> (EditorState, PathBuf) {
|
|||
let dir = fresh_dir();
|
||||
let f = dir.join("a.lean");
|
||||
std::fs::write(&f, "").unwrap();
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
let fd = f.display().to_string();
|
||||
exec(&s, &format!("pmacs.buffer.find_or_open({fd:?})"));
|
||||
|
|
@ -543,7 +543,7 @@ fn no_abbreviation_state_is_opened_outside_a_lean_buffer() {
|
|||
let dir = fresh_dir();
|
||||
let f = dir.join("a.rs");
|
||||
std::fs::write(&f, "").unwrap();
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
let fd = f.display().to_string();
|
||||
exec(&s, &format!("pmacs.buffer.find_or_open({fd:?})"));
|
||||
|
|
@ -811,7 +811,7 @@ fn the_expansion_reaches_the_first_did_change() {
|
|||
|
||||
let f = dir.join("a.lean");
|
||||
std::fs::write(&f, "").unwrap();
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -1018,3 +1018,10 @@ fn detaching_a_frontend_purges_only_its_own_pending_state() {
|
|||
"B's detachment purged B's entries and left A's record valid"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ fn probe(s: &EditorState) -> (String, String, i64, Option<String>) {
|
|||
|
||||
#[test]
|
||||
fn open_seats_cursor_and_ret_visits_the_row() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
let (name, text, line, _) = probe(&s);
|
||||
assert_eq!(name, "*test-panel*");
|
||||
|
|
@ -227,7 +227,7 @@ fn open_seats_cursor_and_ret_visits_the_row() {
|
|||
|
||||
#[test]
|
||||
fn header_row_is_not_visitable() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('p')); // up onto the header
|
||||
press(&mut s, KeyCode::Enter);
|
||||
|
|
@ -237,7 +237,7 @@ fn header_row_is_not_visitable() {
|
|||
|
||||
#[test]
|
||||
fn q_restores_the_previous_buffer() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('q'));
|
||||
let (name, _, _, _) = probe(&s);
|
||||
|
|
@ -246,7 +246,7 @@ fn q_restores_the_previous_buffer() {
|
|||
|
||||
#[test]
|
||||
fn panel_rejects_typing() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
let (_, before, _, _) = probe(&s);
|
||||
press(&mut s, KeyCode::Char('z')); // unbound printable → self-insert → intercept rejects
|
||||
|
|
@ -258,7 +258,7 @@ fn panel_rejects_typing() {
|
|||
fn dispatch_idle_is_false_while_a_panel_is_focused() {
|
||||
// Q#P6: while the panel is the active buffer, semantic frontends
|
||||
// must round-trip every key (RET = visit, not an optimistic \n).
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
assert!(s.dispatch_idle(), "scratch buffer: idle");
|
||||
open_test_panel(&mut s);
|
||||
assert!(!s.dispatch_idle(), "panel focused: keys must round-trip");
|
||||
|
|
@ -268,7 +268,7 @@ fn dispatch_idle_is_false_while_a_panel_is_focused() {
|
|||
|
||||
#[test]
|
||||
fn refresh_reruns_the_source_and_reseats() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('g'));
|
||||
let (_, text, line, _) = probe(&s);
|
||||
|
|
@ -304,7 +304,7 @@ const PANEL_TEXT: &str = "3 items RET visit q quit\nalpha\nbeta\ngamma";
|
|||
/// consulting the intercept chain.
|
||||
#[test]
|
||||
fn s1_1_the_undo_chord_cannot_empty_a_listview_panel() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
assert_eq!(active_text(&s), PANEL_TEXT, "precondition: rendered");
|
||||
|
||||
|
|
@ -327,7 +327,7 @@ fn s1_1_the_undo_chord_cannot_empty_a_listview_panel() {
|
|||
/// *Bite:* same empty result on the pre-image.
|
||||
#[test]
|
||||
fn s1_2_m_x_buffer_undo_cannot_empty_a_listview_panel() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
|
||||
m_x(&mut s, "buffer.undo");
|
||||
|
|
@ -355,7 +355,7 @@ fn s1_2_m_x_buffer_undo_cannot_empty_a_listview_panel() {
|
|||
/// raising.
|
||||
#[test]
|
||||
fn s1_4_the_owners_refresh_still_works_after_the_lock() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
let panel = id_of(&s, "*test-panel*");
|
||||
assert!(
|
||||
|
|
@ -388,7 +388,7 @@ fn s1_4_the_owners_refresh_still_works_after_the_lock() {
|
|||
/// therefore passes the rope half and fails the lifted half.
|
||||
#[test]
|
||||
fn s1_5_the_rope_lock_and_named_intercept_refuse_in_order() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
let panel = id_of(&s, "*test-panel*");
|
||||
let before = active_text(&s);
|
||||
|
|
@ -453,7 +453,7 @@ fn s1_5_the_rope_lock_and_named_intercept_refuse_in_order() {
|
|||
/// pinned through `dispatch_idle_for` rather than through `read_only`.
|
||||
#[test]
|
||||
fn s1_6_round_trip_input_survives_the_adoption() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
|
||||
// (a) the premise.
|
||||
|
|
@ -498,7 +498,7 @@ fn s1_6_round_trip_input_survives_the_adoption() {
|
|||
/// the old line index live and this paint assertion bites.
|
||||
#[test]
|
||||
fn s1_7_a_shrinking_refresh_reaches_the_window() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_test_panel(&mut s);
|
||||
let painted = paint_active_window(&s, 6, 24);
|
||||
assert_eq!(
|
||||
|
|
@ -540,7 +540,7 @@ fn s1_7_a_shrinking_refresh_reaches_the_window() {
|
|||
/// builtin/runtime/listview.lua` falsifies it.
|
||||
#[test]
|
||||
fn s1_9_a_foreign_buffer_with_the_panels_name_is_never_adopted() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
|
||||
|
|
@ -585,7 +585,7 @@ fn s1_9_a_foreign_buffer_with_the_panels_name_is_never_adopted() {
|
|||
/// created.
|
||||
#[test]
|
||||
fn s1_10_the_disambiguation_limit_raises_rather_than_adopting() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"MINE = pmacs.buffer.create('*test-panel*')\n\
|
||||
|
|
@ -624,7 +624,7 @@ fn s1_10_the_disambiguation_limit_raises_rather_than_adopting() {
|
|||
/// command produced, never on "it did not raise".
|
||||
#[test]
|
||||
fn s1_11_a_disambiguated_panel_still_answers_ret_g_and_q() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
|
||||
|
|
@ -678,7 +678,7 @@ fn s1_11_a_disambiguated_panel_still_answers_ret_g_and_q() {
|
|||
/// the disambiguation and `q` lands back in `*test-panel*<2>`.
|
||||
#[test]
|
||||
fn s1_12_the_q_target_capture_is_not_inverted_across_two_panels() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&s,
|
||||
"FOREIGN = pmacs.buffer.create('*test-panel*')\n\
|
||||
|
|
@ -755,3 +755,10 @@ fn s1_14_no_bypass_write_or_name_keyed_identity_remains() {
|
|||
"every `panels[` subscript must be an append; found {subscripts:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ fn fake_lsp_path() -> String {
|
|||
/// A fresh editor with the shipped language configs cleared, so the only
|
||||
/// server any test can spawn is the fake one it configures itself.
|
||||
fn editor() -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
state
|
||||
}
|
||||
|
|
@ -722,3 +722,10 @@ fn acc34a_canonicalize_declines_a_non_utf8_resolution() {
|
|||
return a U+FFFD-substituted path that exists nowhere"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fn fake_lsp_path() -> String {
|
|||
/// A fresh editor with the shipped language configs cleared, so the only
|
||||
/// server any test can spawn is the fake one it configures itself.
|
||||
fn editor() -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
state
|
||||
}
|
||||
|
|
@ -702,3 +702,10 @@ fn a_resolver_returning_nil_declines_silently() {
|
|||
file_uri(&fx.dir("proj"))
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ fn open(state: &EditorState, path: &Path) {
|
|||
}
|
||||
|
||||
fn editor_for(dir: &Path) -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Clamp detection so a stray marker above the tempdir cannot leak in.
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -565,3 +565,10 @@ fn j1b2_preservation_a_spawnable_server_still_attaches() {
|
|||
);
|
||||
assert_eq!(failure_count(&state), 0);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ fn assert_disjoint_within(ranges: &[ByteRange], vp: ByteRange) {
|
|||
|
||||
#[test]
|
||||
fn incremental_reconstruction_equals_fresh_full_projection() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let buffer_id = active_buffer(&state);
|
||||
let vp1 = ByteRange { start: 0, end: 64 };
|
||||
|
||||
|
|
@ -327,3 +327,11 @@ fn daemon_routes_semantic_family_to_semantic_session_only() {
|
|||
"grid session must NOT receive the semantic family"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ fn m4_1_parse_tree_introspectable_via_lua() {
|
|||
use pmacs::editor::EditorState;
|
||||
use pmacs::lua_bindings::BufferIdLua;
|
||||
|
||||
let editor = EditorState::new();
|
||||
let editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Insert a buffer with known shape; bind its handle into Lua as
|
||||
// `BUF` so the script can reference it by name.
|
||||
let buf_id = editor
|
||||
|
|
@ -359,7 +359,8 @@ fn m4_2_opening_a_rust_file_produces_a_parse_tree() {
|
|||
let path = dir.path().join("hello.rs");
|
||||
std::fs::write(&path, b"fn main() { let x = 1 + 2; }\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::open(path).expect("open .rs");
|
||||
let mut state =
|
||||
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open .rs");
|
||||
pump_async(&mut state, |s| current_tree_language(s).is_some());
|
||||
assert_eq!(current_tree_language(&state).as_deref(), Some("rust"));
|
||||
|
||||
|
|
@ -389,7 +390,8 @@ fn m4_2_opening_a_lua_file_produces_a_parse_tree() {
|
|||
let path = dir.path().join("hello.lua");
|
||||
std::fs::write(&path, b"local x = 1\nreturn x + 2\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::open(path).expect("open .lua");
|
||||
let mut state =
|
||||
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open .lua");
|
||||
pump_async(&mut state, |s| current_tree_language(s).is_some());
|
||||
assert_eq!(current_tree_language(&state).as_deref(), Some("lua"));
|
||||
|
||||
|
|
@ -420,7 +422,8 @@ fn m4_2_register_extension_attaches_for_a_runtime_added_extension() {
|
|||
let path = dir.path().join("hello.myrust");
|
||||
std::fs::write(&path, b"fn main() {}\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::open(path).expect("open .myrust");
|
||||
let mut state = pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots())
|
||||
.expect("open .myrust");
|
||||
// The auto-attach hook fires *during* open, but at that point our
|
||||
// custom extension isn't registered yet --- so the first hook
|
||||
// invocation is a no-op. Register the extension and trigger the
|
||||
|
|
@ -516,7 +519,8 @@ fn render_active_window_to_grid(
|
|||
/// either the parse settles (so highlights can attach) or the
|
||||
/// timeout deadline hits.
|
||||
fn open_and_wait_for_parse(path: std::path::PathBuf) -> pmacs::editor::EditorState {
|
||||
let mut state = pmacs::editor::EditorState::open(path).expect("open file");
|
||||
let mut state =
|
||||
pmacs::editor::EditorState::open_with_roots(path, &crate::iso::roots()).expect("open file");
|
||||
pump_async(&mut state, |s| current_tree_language(s).is_some());
|
||||
state
|
||||
}
|
||||
|
|
@ -1155,7 +1159,7 @@ fn m4_4_pty_mode_child_observes_a_tty() {
|
|||
/// to Lua" spec invariant for the process case.
|
||||
#[test]
|
||||
fn m4_4_lua_surface_drives_lifecycle() {
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let id_raw: i64 = state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -1611,7 +1615,7 @@ fn m4_5_protocol_violation_surfaces_as_structured_error() {
|
|||
fn m4_5_lua_surface_drives_lsp_lifecycle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
let sid_raw: u64 = lua
|
||||
|
|
@ -1846,7 +1850,7 @@ fn m4_6_diagnostic_source_field_is_preserved() {
|
|||
fn m4_6_lua_surface_reads_diagnostics() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
let sid_raw: u64 = lua
|
||||
|
|
@ -1958,7 +1962,7 @@ fn m4_6_lua_surface_reads_diagnostics() {
|
|||
fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let commands: Vec<String> = lua
|
||||
|
|
@ -2028,7 +2032,7 @@ fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
|
|||
fn m4_6_diag_attach_view_pushes_diagnostic_overlay() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let attached: bool = state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -2260,7 +2264,7 @@ fn m4_7_signature_help_during_function_call() {
|
|||
fn m4_7_lua_surface_drives_completion_hover_signature() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
let sid_raw: u64 = lua
|
||||
|
|
@ -2581,7 +2585,7 @@ fn m4_8_status_buffer_text_reflects_state_and_capabilities() {
|
|||
fn m4_8_lua_surface_exposes_status() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
let sid_raw: u64 = lua
|
||||
|
|
@ -2761,7 +2765,7 @@ fn m4_9_project_switch_is_first_class() {
|
|||
touch_file(&d1.path().join("Cargo.toml"));
|
||||
touch_file(&d2.path().join(".luarc.json"));
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
let d1_path = d1.path().display().to_string();
|
||||
let d2_path = d2.path().display().to_string();
|
||||
|
|
@ -2817,7 +2821,7 @@ fn m4_9_lsp_runs_per_project_not_per_buffer() {
|
|||
touch_file(&d1.path().join("Cargo.toml"));
|
||||
touch_file(&d2.path().join("Cargo.toml"));
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let d1_path = d1.path().display().to_string();
|
||||
let d2_path = d2.path().display().to_string();
|
||||
|
||||
|
|
@ -2929,7 +2933,7 @@ fn m4_10_index_persists_across_sessions() {
|
|||
|
||||
// Session A: index a synthetic source file, save to disk.
|
||||
{
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
let saved_path: String = lua
|
||||
.load(format!(
|
||||
|
|
@ -2957,7 +2961,7 @@ fn m4_10_index_persists_across_sessions() {
|
|||
// searches still find what session A indexed --- without ever
|
||||
// calling upsert_file again.
|
||||
let started = std::time::Instant::now();
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
let (file_count, symbol_count, hit_count, hit_name): (u64, u64, u64, String) = lua
|
||||
.load(format!(
|
||||
|
|
@ -3063,7 +3067,7 @@ fn m4_10_lua_surface_drives_index() {
|
|||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let root = dir.path().display().to_string();
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let (files_before, syms_before, hit_count, after_invalidate, generation_after): (
|
||||
|
|
@ -3111,7 +3115,7 @@ fn m4_10_lua_surface_ingests_lsp_workspace_symbol() {
|
|||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let root = dir.path().display().to_string();
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
// The URI we feed has to be under the project root so the
|
||||
// resulting absolute path is deterministic.
|
||||
|
|
@ -3160,7 +3164,7 @@ fn m4_10_lua_surface_ingests_lsp_workspace_symbol() {
|
|||
fn m4_11_multiple_sources_combine_without_duplicates() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let (parse_count, parse_source, total_count): (u64, String, u64) = lua
|
||||
|
|
@ -3235,7 +3239,7 @@ fn m4_11_multiple_sources_combine_without_duplicates() {
|
|||
fn m4_11_source_priority_configurable() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let (winner_first, winner_second): (String, String) = lua
|
||||
|
|
@ -3289,7 +3293,7 @@ fn m4_11_source_priority_configurable() {
|
|||
fn m4_11_custom_sources_from_lua() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let (custom_count_active, custom_label, custom_source, count_disabled, count_unregistered): (
|
||||
|
|
@ -3373,7 +3377,7 @@ fn m4_11_custom_sources_from_lua() {
|
|||
fn m4_11_snippets_surface_through_completion() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
let (label, kind, insert_text, source): (String, String, String, String) = lua
|
||||
.load(
|
||||
|
|
@ -3529,7 +3533,7 @@ fn m4_12_formatting_response_lands_in_store() {
|
|||
fn m4_12_lua_surface_drives_definition_and_formatting() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
// Spawn a server through the Lua surface.
|
||||
|
|
@ -3667,7 +3671,7 @@ fn m4_12_cross_file_go_to_definition_and_jump_back() {
|
|||
let b_disp = b_path.display().to_string();
|
||||
let b_uri = format!("file://{b_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
// Point the default `rust` server at the fake, in `defenv` mode,
|
||||
|
|
@ -3787,7 +3791,7 @@ fn m4_13_rename_applies_cross_file_workspace_edit() {
|
|||
let b_disp = b_path.display().to_string();
|
||||
let b_uri = format!("file://{b_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
state
|
||||
|
|
@ -3915,7 +3919,7 @@ fn m4_14_code_action_command_drives_apply_edit() {
|
|||
std::fs::write(&a_path, b"abcfooxyz\n___zzz\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
state
|
||||
|
|
@ -4028,7 +4032,7 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
|
|||
let created = dir.path().join("created.rs");
|
||||
let b2 = dir.path().join("b2.rs");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
state
|
||||
|
|
@ -4131,7 +4135,7 @@ fn m4_15_workspace_edit_resource_ops_apply_in_order() {
|
|||
/// kinds, and `paddingRight`.
|
||||
#[test]
|
||||
fn m4_16_lua_surface_drives_inlay_hints() {
|
||||
let mut s = pmacs::editor::EditorState::new();
|
||||
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut s, Some("inlaybounds"));
|
||||
|
||||
let uri = "file:///tmp/m4_16_inlay.rs";
|
||||
|
|
@ -4190,7 +4194,7 @@ fn m4_16_lua_surface_drives_inlay_hints() {
|
|||
/// the `token_type` index resolves to a name.
|
||||
#[test]
|
||||
fn m4_17_lua_surface_drives_semantic_tokens() {
|
||||
let mut s = pmacs::editor::EditorState::new();
|
||||
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut s, None);
|
||||
|
||||
let uri = "file:///tmp/m4_17_sem.rs";
|
||||
|
|
@ -4261,7 +4265,7 @@ fn m4_18_inlay_hint_refresh_repulls_via_server_request() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4314,7 +4318,7 @@ fn m4_18b_inlay_hints_auto_pull_after_initialize() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4365,7 +4369,7 @@ fn m4_19_semantic_tokens_refresh_repulls_via_server_request() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4422,7 +4426,7 @@ fn arc1c_semantic_tokens_auto_pull_on_attach() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4466,7 +4470,7 @@ fn arc1c_semantic_tokens_repull_after_edit_flush() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4529,7 +4533,7 @@ fn arc1d_signature_help_auto_triggers_on_trigger_char() {
|
|||
std::fs::write(&a_path, b"\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4589,7 +4593,7 @@ fn arc1d_signature_help_does_not_trigger_on_ordinary_typing() {
|
|||
std::fs::write(&a_path, b"\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4648,7 +4652,7 @@ fn arc1c_range_only_server_is_served_range_requests() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4703,7 +4707,7 @@ fn arc1c_range_only_utf16_server_gets_converted_bounds() {
|
|||
std::fs::write(&a_path, "fn a() {}\nlet x = \u{e9}\u{e9};".as_bytes()).expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4752,7 +4756,7 @@ fn arc1d_signature_help_triggers_on_non_ascii_trigger_char() {
|
|||
std::fs::write(&a_path, b"\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4810,7 +4814,7 @@ fn arc1d_signature_help_ignores_non_typed_edits() {
|
|||
std::fs::write(&a_path, b"\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4881,7 +4885,7 @@ fn arc1c_full_only_server_repulls_via_full_not_delta() {
|
|||
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -4954,7 +4958,7 @@ fn arc1c_full_only_server_repulls_via_full_not_delta() {
|
|||
/// fake returns one token.
|
||||
#[test]
|
||||
fn m4_20_semantic_tokens_range() {
|
||||
let mut s = pmacs::editor::EditorState::new();
|
||||
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut s, None);
|
||||
|
||||
let uri = "file:///tmp/m4_20_sem.rs";
|
||||
|
|
@ -4998,7 +5002,7 @@ fn m4_20_semantic_tokens_range() {
|
|||
/// `resultId` "rid-2".
|
||||
#[test]
|
||||
fn m4_21_semantic_tokens_full_then_delta() {
|
||||
let mut s = pmacs::editor::EditorState::new();
|
||||
let mut s = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut s, None);
|
||||
|
||||
let uri = "file:///tmp/m4_21_sem.rs";
|
||||
|
|
@ -5078,7 +5082,7 @@ fn m4_22_rename_prepare_gates_and_prefills() {
|
|||
std::fs::write(&a_path, b"abcfooxyz\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -5179,7 +5183,7 @@ fn m4_23_rename_prepare_refusal_aborts() {
|
|||
std::fs::write(&a_path, b"abcfooxyz\n").expect("write a");
|
||||
let a_disp = a_path.display().to_string();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -5272,7 +5276,7 @@ fn m4_24_workspace_did_change_watched_files() {
|
|||
let received = base.join(".received");
|
||||
let foo_uri = format!("file://{}", base.join("foo.txt").display());
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -5351,7 +5355,7 @@ fn m4_24_workspace_did_change_watched_files() {
|
|||
fn m4_25_tier1_language_server_configs_and_filetypes() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let probe: mlua::Table = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -5455,7 +5459,7 @@ fn m4_26_auto_attach_roots_server_at_opened_files_project() {
|
|||
let go_file_disp = go_file.display().to_string();
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Clamp the marker walk to the tempdir so a stray ancestor marker
|
||||
// (a developer's /tmp/.git, say) can't masquerade as the root.
|
||||
// Point the default `go` server at the fake in `rooturi` mode.
|
||||
|
|
@ -5567,7 +5571,7 @@ fn m4_27_real_gopls_analyzes_module_via_auto_attach() {
|
|||
let go_file_disp = go_file.display().to_string();
|
||||
let uri = format!("file://{go_file_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// gopls handshake + workspace load is slow on a cold cache (30s).
|
||||
real_server_open_and_init(&mut state, "go", &gopls, &root_disp, &go_file_disp);
|
||||
|
||||
|
|
@ -5664,7 +5668,7 @@ fn m4_28_real_clangd_diagnostics_and_semantic_tokens_via_auto_attach() {
|
|||
let cpp_disp = cpp.display().to_string();
|
||||
let uri = format!("file://{cpp_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
real_server_open_and_init(&mut state, "cpp", &clangd, &root_disp, &cpp_disp);
|
||||
|
||||
// Fire a semantic-tokens request; diagnostics flow unsolicited
|
||||
|
|
@ -5767,7 +5771,7 @@ fn m4_29_real_rust_analyzer_inlay_hints_via_auto_attach() {
|
|||
let file_disp = file.display().to_string();
|
||||
let uri = format!("file://{file_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
real_server_open_and_init(&mut state, "rust", &rust_analyzer, &root_disp, &file_disp);
|
||||
|
||||
// rust-analyzer only answers `textDocument/inlayHint` after it has
|
||||
|
|
@ -5815,7 +5819,7 @@ fn m4_29_real_rust_analyzer_inlay_hints_via_auto_attach() {
|
|||
fn m4_12_default_bundle_wires_commands_and_keymaps() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let probe: mlua::Table = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -5870,7 +5874,7 @@ fn m4_12_default_bundle_wires_commands_and_keymaps() {
|
|||
fn m4_12_default_bundle_wires_cuda() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let probe: mlua::Table = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -5926,7 +5930,7 @@ fn m4_12_default_bundle_wires_cuda() {
|
|||
fn m4_12_default_bundle_wires_bash() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let probe: mlua::Table = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -5970,7 +5974,7 @@ fn m4_12_default_bundle_wires_bash() {
|
|||
#[test]
|
||||
fn m4_shebang_resolver_maps_interpreters() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let resolve = |first_line: &str| -> Option<String> {
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -6030,7 +6034,7 @@ fn m4_shebang_resolver_maps_interpreters() {
|
|||
#[test]
|
||||
fn m4_shebang_extensionless_script_resolves_bash() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let hook = dir.path().join("pre-commit"); // no extension
|
||||
std::fs::write(&hook, b"#!/bin/sh\nset -e\necho building\n").expect("write");
|
||||
|
|
@ -6071,7 +6075,7 @@ fn m4_shebang_extensionless_script_resolves_bash() {
|
|||
#[test]
|
||||
fn m4_shebang_does_not_override_extension() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let f = dir.path().join("tool.py");
|
||||
std::fs::write(&f, b"#!/bin/sh\nprint('hi')\n").expect("write");
|
||||
|
|
@ -6116,7 +6120,7 @@ fn m4_shebang_does_not_override_extension() {
|
|||
#[test]
|
||||
fn m4_shebang_extensionless_grammarless_language_is_silent() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let f = dir.path().join("generate"); // no extension
|
||||
// `ruby` is deliberately grammarless (and serverless) — a language the
|
||||
|
|
@ -6166,7 +6170,7 @@ fn m4_shebang_extensionless_grammarless_language_is_silent() {
|
|||
#[test]
|
||||
fn m4_shebang_edit_keeps_pinned_grammar() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let hook = dir.path().join("deploy"); // no extension
|
||||
std::fs::write(&hook, b"#!/bin/sh\necho one\n").expect("write");
|
||||
|
|
@ -6261,7 +6265,7 @@ fn m4_shebang_edit_keeps_pinned_grammar() {
|
|||
#[test]
|
||||
fn m4_modeline_overrides_extension_end_to_end() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let file = dir.path().join("misleading.py");
|
||||
std::fs::write(&file, b"-- -*- mode: lua -*-\nprint('ok')\n").expect("write");
|
||||
|
|
@ -6299,7 +6303,7 @@ fn m4_modeline_overrides_extension_end_to_end() {
|
|||
#[test]
|
||||
fn m4_modeline_parser_matches_supported_emacs_and_vim_forms() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let resolve = |text: &str| -> Option<String> {
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -6344,7 +6348,7 @@ fn m4_modeline_parser_matches_supported_emacs_and_vim_forms() {
|
|||
#[test]
|
||||
fn m4_modeline_parser_enforces_boundaries_aliases_and_conflicts() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let resolve = |text: &str| -> Option<String> {
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -6428,7 +6432,7 @@ fn m4_modeline_parser_enforces_boundaries_aliases_and_conflicts() {
|
|||
#[test]
|
||||
fn m4_modeline_unknown_mode_is_quiet_and_parser_free() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let file = dir.path().join("notes.txt");
|
||||
std::fs::write(&file, b"# vim:ft=prose:\nhello\n").expect("write");
|
||||
|
|
@ -6464,7 +6468,7 @@ fn m4_modeline_unknown_mode_is_quiet_and_parser_free() {
|
|||
#[test]
|
||||
fn m4_modeline_language_is_pinned_until_reopen() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let file = dir.path().join("mutable.txt");
|
||||
let other = dir.path().join("other.txt");
|
||||
|
|
@ -6558,7 +6562,7 @@ fn m4_modeline_language_is_pinned_until_reopen() {
|
|||
#[test]
|
||||
fn m4_modeline_shared_resolver_preserves_pathless_lsp_guard() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let (syntax, lsp): (Option<String>, Option<String>) = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -6580,7 +6584,7 @@ fn m4_modeline_shared_resolver_preserves_pathless_lsp_guard() {
|
|||
#[test]
|
||||
fn m4_filename_map_resolves_special_files() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let probe: mlua::Table = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -6650,7 +6654,7 @@ fn m4_filename_map_resolves_special_files() {
|
|||
#[test]
|
||||
fn m4_filename_extensionless_dockerfile_highlights() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let f = dir.path().join("Dockerfile"); // no extension
|
||||
std::fs::write(&f, b"FROM alpine:3\nRUN apk add curl\n").expect("write");
|
||||
|
|
@ -6690,7 +6694,7 @@ fn m4_filename_extensionless_dockerfile_highlights() {
|
|||
#[test]
|
||||
fn m4_gap_grammars_align_with_lsp_configs() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
for (path, id) in [
|
||||
("app.py", "python"),
|
||||
("srv.go", "go"),
|
||||
|
|
@ -6733,7 +6737,7 @@ fn m4_gap_grammars_align_with_lsp_configs() {
|
|||
#[test]
|
||||
fn m4_json_yaml_lsp_configs_pin_command_and_sections() {
|
||||
use pmacs::editor::EditorState;
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = s.lua_host.lua();
|
||||
|
||||
// json: the `@t1ckbase/vscode-langservers-extracted@2.0.2` binary
|
||||
|
|
@ -6809,7 +6813,7 @@ fn m4_json_yaml_lsp_configs_pin_command_and_sections() {
|
|||
#[test]
|
||||
fn m4_5_initial_config_pushed_via_did_change_configuration() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let sink = dir.path().join("config.jsonl");
|
||||
|
|
@ -6895,7 +6899,7 @@ fn m4_real_json_provider_receives_config_and_reports_diagnostics() {
|
|||
let file_disp = file.display().to_string();
|
||||
let uri = format!("file://{file_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -6964,7 +6968,7 @@ fn m4_real_yaml_provider_pulls_config_and_reports_diagnostics() {
|
|||
let file_disp = file.display().to_string();
|
||||
let uri = format!("file://{file_disp}");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -7043,7 +7047,7 @@ fn m4_real_yaml_provider_pulls_config_and_reports_diagnostics() {
|
|||
fn m4_lua_bundle_debounces_did_change_per_keystroke() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let file = dir.path().join("debounce.rs");
|
||||
|
|
@ -7144,12 +7148,12 @@ fn m4_lua_bundle_debounces_did_change_per_keystroke() {
|
|||
fn m4_12_default_bundle_after_load_robust_to_missing_server() {
|
||||
use pmacs::editor::EditorState;
|
||||
let _dir = tempfile::TempDir::new().unwrap();
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Plain after-load with no buffer path attached — the hook should
|
||||
// run, find no language for path=nil, and exit silently. If it
|
||||
// throws, the *errors* buffer would record it; verify it doesn't.
|
||||
s.lua_host.hooks();
|
||||
let s2 = EditorState::new();
|
||||
let s2 = EditorState::new_with_roots(&crate::iso::roots());
|
||||
drop(s);
|
||||
let saw_error: bool = s2
|
||||
.lua_host
|
||||
|
|
@ -7199,7 +7203,7 @@ fn project_set_search_boundary_clamps_lua_detect_call() {
|
|||
let f = workspace.join("src/main.rs");
|
||||
std::fs::write(&f, b"").expect("touch file");
|
||||
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
let workspace_str = workspace.display().to_string();
|
||||
let f_str = f.display().to_string();
|
||||
|
|
@ -7234,7 +7238,7 @@ fn project_search_boundary_round_trips_via_lua() {
|
|||
|
||||
let dir = tempfile::tempdir().expect("dir");
|
||||
let dir_str = dir.path().display().to_string();
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
let (initial, after_set, after_clear): (Option<String>, Option<String>, Option<String>) = lua
|
||||
|
|
@ -7420,7 +7424,7 @@ fn spawn_lsp_and_init(state: &mut pmacs::editor::EditorState, mode: Option<&str>
|
|||
#[test]
|
||||
fn m4_5_await_completion_returns_result_and_populates_store() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7456,7 +7460,7 @@ fn m4_5_await_completion_returns_result_and_populates_store() {
|
|||
#[test]
|
||||
fn m4_5_await_server_error_raises_failed() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("error"));
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7496,7 +7500,7 @@ fn m4_5_await_server_error_raises_failed() {
|
|||
#[test]
|
||||
fn m4_5_await_cancelled_when_server_stops_mid_request() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("silent"));
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7534,7 +7538,7 @@ fn m4_5_await_cancelled_when_server_stops_mid_request() {
|
|||
#[test]
|
||||
fn m4_5_await_times_out_against_silent_server() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("silent"));
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7576,7 +7580,7 @@ fn m4_5_await_times_out_against_silent_server() {
|
|||
#[test]
|
||||
fn m4_5_await_superseded_request_is_cancelled() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("silent"));
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7622,7 +7626,7 @@ fn m4_5_await_superseded_request_is_cancelled() {
|
|||
#[test]
|
||||
fn m4_5_await_resolves_same_frame_as_response_absorbed() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7710,7 +7714,7 @@ fn m4_5_await_resolves_same_frame_as_response_absorbed() {
|
|||
#[test]
|
||||
fn m4_5_position_encoding_utf16_round_trips_non_ascii() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("posecho"));
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7766,7 +7770,7 @@ fn m4_5_position_encoding_utf16_round_trips_non_ascii() {
|
|||
fn m4_5_utf16_rename_and_prepare_rename_convert_positions() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, Some("posecho"));
|
||||
let uri = "file:///tmp/m4_5_rename_utf16.rs";
|
||||
state
|
||||
|
|
@ -7806,7 +7810,7 @@ fn m4_5_utf16_rename_and_prepare_rename_convert_positions() {
|
|||
#[test]
|
||||
fn m4_5_workspace_configuration_answered_from_settings() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7863,7 +7867,7 @@ fn m4_5_workspace_configuration_answered_from_settings() {
|
|||
#[test]
|
||||
fn m4_5_location_nav_requests_route_by_kind() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7913,7 +7917,7 @@ fn m4_5_location_nav_requests_route_by_kind() {
|
|||
#[test]
|
||||
fn m4_5_symbols_and_highlight_round_trip() {
|
||||
use pmacs::editor::EditorState;
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_lsp_and_init(&mut state, None);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -7977,7 +7981,7 @@ fn m4_5_symbols_and_highlight_round_trip() {
|
|||
/// Open `path` against the fake server and wait for initialization
|
||||
/// (shared bootstrap for the panel tests).
|
||||
fn open_against_fake(path: &std::path::Path) -> pmacs::editor::EditorState {
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_lsp_path();
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8223,7 +8227,7 @@ fn rd1_delete_refuses_when_a_bound_buffer_is_modified() {
|
|||
let f = dir.path().join("a.rs");
|
||||
std::fs::write(&f, b"original\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &f);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8265,7 +8269,7 @@ fn rd2_delete_still_succeeds_for_a_clean_open_buffer() {
|
|||
let f = dir.path().join("clean.rs");
|
||||
std::fs::write(&f, b"untouched\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &f);
|
||||
|
||||
let (ok, err) = rd_delete(&mut state, &f, "");
|
||||
|
|
@ -8310,7 +8314,7 @@ fn rd3_filesystem_failure_leaves_the_clean_buffer_intact() {
|
|||
let target = dir.path().join("target");
|
||||
std::fs::write(&target, b"was a file\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &target);
|
||||
|
||||
// The path changes type behind pmacs's back — the same class of
|
||||
|
|
@ -8354,7 +8358,7 @@ fn rd4_on_removed_observes_the_path_already_gone() {
|
|||
std::fs::write(&f, b"bye\n").expect("write");
|
||||
let p = f.display().to_string();
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &f);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8399,7 +8403,7 @@ fn rd5_delete_from_inside_the_targets_own_intercept_refuses_before_disk() {
|
|||
std::fs::write(&f, b"busy\n").expect("write");
|
||||
let p = f.display().to_string();
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &f);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8455,7 +8459,7 @@ fn rd6_a_clean_first_match_cannot_hide_a_modified_duplicate() {
|
|||
std::fs::write(&f, b"shared\n").expect("write");
|
||||
let p = f.display().to_string();
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -8493,7 +8497,7 @@ fn rd7_a_sibling_directory_sharing_a_name_prefix_does_not_block() {
|
|||
let outside = sibling.join("out.rs");
|
||||
std::fs::write(&outside, b"out\n").expect("write out");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &outside);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8527,7 +8531,7 @@ fn rd8_recursive_delete_refuses_for_a_modified_descendant() {
|
|||
let inner = nested.join("deep.rs");
|
||||
std::fs::write(&inner, b"deep\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &inner);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8575,7 +8579,7 @@ fn rd9_clean_recursive_delete_reconciles_descendants_through_both_phases() {
|
|||
let inner = tree.join("kept.rs");
|
||||
std::fs::write(&inner, b"kept\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &inner);
|
||||
// Display it, so the phase-1 window redirect has something to do.
|
||||
state
|
||||
|
|
@ -8629,7 +8633,7 @@ fn rd10_absent_plus_ignore_does_not_destroy_a_modified_buffer() {
|
|||
let f = dir.path().join("vanished.rs");
|
||||
std::fs::write(&f, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &f);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -8684,7 +8688,7 @@ fn rd14_clean_duplicates_all_reconcile() {
|
|||
std::fs::write(&f, b"twin\n").expect("write");
|
||||
let p = f.display().to_string();
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -8940,7 +8944,7 @@ fn rd11_absent_plus_ignore_succeeds_through_the_server_pump() {
|
|||
let victim = dir.path().join("victim.rs");
|
||||
std::fs::write(&victim, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "delete", "uri": rd_uri(&victim),
|
||||
|
|
@ -8991,7 +8995,7 @@ fn rd11a_present_plus_ignore_with_a_modified_buffer_is_refused() {
|
|||
let victim = dir.path().join("victim.rs");
|
||||
std::fs::write(&victim, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "delete", "uri": rd_uri(&victim),
|
||||
|
|
@ -9049,7 +9053,7 @@ fn rd11b_a_dangling_symlink_counts_as_present() {
|
|||
std::fs::write(&real, b"content\n").expect("write");
|
||||
std::os::unix::fs::symlink(&real, &link).expect("symlink");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "delete", "uri": rd_uri(&link),
|
||||
|
|
@ -9113,7 +9117,7 @@ fn rd11c_absent_without_ignore_refuses_before_the_earlier_op_runs() {
|
|||
let missing = dir.path().join("never-existed.rs");
|
||||
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
rd_edit_op(&a, 0, 3, 6, "EDITED"),
|
||||
|
|
@ -9167,7 +9171,7 @@ fn rd11d_an_unanswerable_stat_fails_closed_in_the_plan() {
|
|||
std::fs::write(¬_a_dir, b"I am a file\n").expect("write the would-be parent");
|
||||
let target = not_a_dir.join("child.rs");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
rd_edit_op(&a, 0, 3, 6, "EDITED"),
|
||||
|
|
@ -9223,7 +9227,7 @@ fn rd12a_edit_then_delete_answers_the_server_and_reports_partial_work() {
|
|||
let a = dir.path().join("a.rs");
|
||||
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
rd_edit_op(&a, 0, 3, 6, "EDITED"),
|
||||
|
|
@ -9276,7 +9280,7 @@ fn rd12b_rename_into_delete_answers_the_server_and_reports_partial_work() {
|
|||
let inside = tree.join("m.rs");
|
||||
std::fs::write(&outside, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "rename", "oldUri": rd_uri(&outside), "newUri": rd_uri(&inside) },
|
||||
|
|
@ -9341,7 +9345,7 @@ fn rd13_the_refusal_answers_the_server_and_leaves_a_durable_trace() {
|
|||
let victim = dir.path().join("victim.rs");
|
||||
std::fs::write(&victim, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [ { "kind": "delete", "uri": rd_uri(&victim) } ]
|
||||
});
|
||||
|
|
@ -9411,7 +9415,7 @@ fn rd15_defensive_a_parse_failure_still_attempts_a_response() {
|
|||
let a = dir.path().join("a.rs");
|
||||
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
// A plan that would otherwise succeed, so a response saying
|
||||
// `applied = false` can only have come from the parse stub.
|
||||
let plan = serde_json::json!({
|
||||
|
|
@ -9476,7 +9480,7 @@ fn rd18_non_recursive_delete_is_not_blocked_by_an_orphan_beneath_it() {
|
|||
let gone = tree.join("gone.rs");
|
||||
std::fs::write(&gone, b"content\n").expect("write");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
rd_open(&mut state, "B", &gone);
|
||||
state
|
||||
.lua_host
|
||||
|
|
@ -9521,7 +9525,7 @@ fn rd19a_create_then_delete_is_not_refused_by_the_plan_time_preflight() {
|
|||
let transient = dir.path().join("transient.rs");
|
||||
let witness = dir.path().join("witness.rs");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "create", "uri": rd_uri(&transient) },
|
||||
|
|
@ -9563,7 +9567,7 @@ fn rd19b_rename_then_delete_is_not_refused_by_the_plan_time_preflight() {
|
|||
let destination = dir.path().join("destination.rs");
|
||||
std::fs::write(&source, b"moving\n").expect("write source");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "rename", "oldUri": rd_uri(&source), "newUri": rd_uri(&destination) },
|
||||
|
|
@ -9607,7 +9611,7 @@ fn rd19c_deferring_the_check_does_not_skip_it() {
|
|||
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
|
||||
let fresh = dir.path().join("fresh.rs");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "create", "uri": rd_uri(&fresh) },
|
||||
|
|
@ -9671,7 +9675,7 @@ fn rd20_the_user_facing_message_reports_partial_application() {
|
|||
let a = dir.path().join("a.rs");
|
||||
std::fs::write(&a, b"abcfooxyz\n").expect("write a");
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
rd_edit_op(&a, 0, 3, 6, "EDITED"),
|
||||
|
|
@ -9753,7 +9757,7 @@ fn rd21_equivalent_dot_path_create_then_delete_is_not_preflight_refused() {
|
|||
"fixture: the URI spellings must differ"
|
||||
);
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [
|
||||
{ "kind": "create", "uri": dot_uri },
|
||||
|
|
@ -9792,7 +9796,7 @@ fn rd22a_partial_edits_inside_one_item_are_reported_conservatively() {
|
|||
let target = dir.path().join("target.rs");
|
||||
std::fs::write(&target, b"abcdef\n").expect("write target");
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [{
|
||||
"textDocument": { "uri": rd_uri(&target), "version": 1 },
|
||||
|
|
@ -9877,7 +9881,7 @@ fn rd22b_a_failing_resource_item_can_leave_filesystem_state() {
|
|||
"fixture: destination parent must start absent"
|
||||
);
|
||||
|
||||
let mut state = pmacs::editor::EditorState::new();
|
||||
let mut state = pmacs::editor::EditorState::new_with_roots(&crate::iso::roots());
|
||||
let plan = serde_json::json!({
|
||||
"documentChanges": [{
|
||||
"kind": "rename",
|
||||
|
|
@ -9907,3 +9911,10 @@ fn rd22b_a_failing_resource_item_can_leave_filesystem_state() {
|
|||
"the response must not deny the directory left on disk: {reason:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -253,6 +253,10 @@ fn second_daemon_same_socket_fails_clearly() {
|
|||
.arg(daemon_a.socket_path())
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@ fn spawn_pmacs_daemon(socket_path: &Path) -> Child {
|
|||
.arg(socket_path)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
|
|
@ -270,6 +274,10 @@ fn cli_attach_invokes_test_ssh_with_expected_argv() {
|
|||
.env(PMACS_TEST_SSH_BIN, &fake_ssh)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -323,6 +331,10 @@ fn cli_attach_with_user_and_instance_name_passes_through_dash_l_and_dash_dash_so
|
|||
.env(PMACS_TEST_SSH_BIN, &fake_ssh)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -361,6 +373,10 @@ fn spawn_bridge(socket: &Path, isolated_home: &Path) -> (Child, ChildStdin, Chil
|
|||
.arg(socket)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -466,6 +482,10 @@ fn cli_attach_with_missing_ssh_binary_surfaces_spawn_failure() {
|
|||
.env(PMACS_TEST_SSH_BIN, &missing_ssh)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ fn handshake_retry_cap_fires_after_three_failed_handshakes() {
|
|||
.env(PMACS_TEST_BACKOFF_SCALE_MS, "1")
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -198,6 +202,10 @@ fn backoff_scaling_observable_in_wall_clock_runtime() {
|
|||
.env(PMACS_TEST_BACKOFF_SCALE_MS, scale_ms)
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
|
|
@ -278,6 +286,10 @@ fn ssh_stderr_from_handshake_attempts_reaches_user() {
|
|||
.env(PMACS_TEST_BACKOFF_SCALE_MS, "1")
|
||||
.env("HOME", isolated_home)
|
||||
.env("XDG_CONFIG_HOME", isolated_home)
|
||||
.env("XDG_DATA_HOME", isolated_home)
|
||||
.env("XDG_STATE_HOME", isolated_home)
|
||||
.env("PMACS_STATE_HOME", isolated_home)
|
||||
.env("XDG_CACHE_HOME", isolated_home)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ impl TestDaemon {
|
|||
.arg(&socket_path)
|
||||
.env("HOME", tempdir.path())
|
||||
.env("XDG_CONFIG_HOME", tempdir.path())
|
||||
.env("XDG_DATA_HOME", tempdir.path())
|
||||
.env("XDG_STATE_HOME", tempdir.path())
|
||||
.env("PMACS_STATE_HOME", tempdir.path())
|
||||
.env("XDG_CACHE_HOME", tempdir.path())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ use pmacs::lua_bindings::BufferIdLua;
|
|||
/// translate them into typed Rust errors because the chunk's
|
||||
/// assertions are the test contract.
|
||||
fn run(chunk: &str) {
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(Some("@m6_4_test"), chunk)
|
||||
|
|
@ -53,7 +53,7 @@ fn run(chunk: &str) {
|
|||
/// Construct a fresh editor and return a captured value (for tests
|
||||
/// that want to do final assertions on the Rust side).
|
||||
fn run_returning<T: mlua::FromLuaMulti>(chunk: &str) -> T {
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(Some("@m6_4_test"), chunk)
|
||||
|
|
@ -438,7 +438,7 @@ fn m6_4_buffer_id_is_a_real_buffer_handle() {
|
|||
// The handle's `buffer_id()` returns a real BufferIdLua that
|
||||
// resolves through the registry. This locks in that the package
|
||||
// is built atop genuinely-public surface; no shadow APIs.
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let returned: mlua::Value = editor
|
||||
.lua_host
|
||||
.eval(
|
||||
|
|
@ -457,3 +457,10 @@ fn m6_4_buffer_id_is_a_real_buffer_handle() {
|
|||
let buf = r.get(id_lua.id()).expect("registered");
|
||||
assert_eq!(buf.name(), "*handle*");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ fn locate_shell(name: &str) -> Option<PathBuf> {
|
|||
/// Construct a fresh editor and run the given Lua chunk against it.
|
||||
fn run(chunk: &str) {
|
||||
let _guard = pump_test_guard();
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(Some("@m6_5_test"), chunk)
|
||||
|
|
@ -95,7 +95,7 @@ fn run(chunk: &str) {
|
|||
/// after-tick contract is exercised end-to-end.
|
||||
fn run_with_pump(setup_chunk: &str, predicate_chunk: &str, timeout_ms: u64) {
|
||||
let _guard = pump_test_guard();
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(Some("@m6_5_setup"), setup_chunk)
|
||||
|
|
@ -528,3 +528,10 @@ fn m6_5_close_terminates_child_and_unregisters() {
|
|||
pmacs.hook.run("process.after-tick")
|
||||
"#);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use pmacs::editor::EditorState;
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn run(chunk: &str) {
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(Some("@m6_7_test"), chunk)
|
||||
|
|
@ -341,3 +341,10 @@ fn m6_7_50_truncation_events_preserve_block_boundaries() {
|
|||
pmacs.repl.config.scrollback_bytes = 16 * 1024 * 1024
|
||||
"#);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ fn m6_8_three_repls_render_independently() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_three_and_wait_running(&mut editor, &lua);
|
||||
|
||||
// Write a unique marker through each REPL: `io.write("MARK_<n>\n")`
|
||||
|
|
@ -246,7 +246,7 @@ fn m6_8_three_repls_respond_independently() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_three_and_wait_running(&mut editor, &lua);
|
||||
|
||||
// Type into h1, switch to h2, type into h2, etc. Each typed marker
|
||||
|
|
@ -308,7 +308,7 @@ fn m6_8_close_one_does_not_affect_others() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_three_and_wait_running(&mut editor, &lua);
|
||||
|
||||
// Capture h2's proc_id before close; Handle:close clears the
|
||||
|
|
@ -375,7 +375,7 @@ fn m6_8_supervisor_reaps_all_children_across_cycles() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
|
||||
// Baseline: list size before any spawning. The post-cycle list
|
||||
// size must equal this — no REPL processes left behind.
|
||||
|
|
@ -480,7 +480,7 @@ fn m6_8_supervisor_reaps_all_children_across_cycles() {
|
|||
/// `pmacs.repl.create` (which the spawn path also calls).
|
||||
#[test]
|
||||
fn m6_8_repls_have_independent_parser_state() {
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(
|
||||
|
|
@ -505,7 +505,7 @@ fn m6_8_repls_have_independent_parser_state() {
|
|||
/// accidentally shared.
|
||||
#[test]
|
||||
fn m6_8_repls_have_independent_scrollback_state() {
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
editor
|
||||
.lua_host
|
||||
.eval(
|
||||
|
|
@ -535,7 +535,7 @@ fn m6_8_buffer_scoped_bindings_route_to_active_buffer() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let setup = format!(
|
||||
r#"
|
||||
_G.h1 = pmacs.repl.spawn {{ argv = {{ "{lua}", "-i" }} }}
|
||||
|
|
@ -618,7 +618,7 @@ fn m6_8_after_tick_hook_drains_all_handles_per_tick() {
|
|||
let Some(lua) = locate_lua() else {
|
||||
return;
|
||||
};
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
spawn_three_and_wait_running(&mut editor, &lua);
|
||||
|
||||
// Capture pre-tick history_end on each handle.
|
||||
|
|
@ -692,3 +692,10 @@ fn m6_8_after_tick_hook_drains_all_handles_per_tick() {
|
|||
.exec()
|
||||
.expect("teardown");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ fn m6_6_sustained_ingest_rate_meets_100mbps_gate() {
|
|||
)
|
||||
.max(1);
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// 100 chars + newline per line. The exact value is unimportant;
|
||||
// what matters is that `yes` blasts at a higher rate than pmacs's
|
||||
// ingest path, so pmacs is the bottleneck under measurement.
|
||||
|
|
@ -389,7 +389,7 @@ fn m6_6_buffer_memory_stays_under_200mb_during_run() {
|
|||
baseline_rss as f64 / (1024.0 * 1024.0)
|
||||
);
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// 100-char line; the line content is unimportant for the ceiling
|
||||
// gate. Total target = 150 MB of bytes-into-history.
|
||||
let line = "a".repeat(100);
|
||||
|
|
@ -526,7 +526,7 @@ fn m6_6_cancel_response_p99_under_100ms() {
|
|||
let delay_ms = MIN_DELAY_MS + (r % (max_delay_ms - MIN_DELAY_MS + 1));
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let _ = spawn_repl(&mut editor, &["yes"]);
|
||||
wait_until_running(&mut editor);
|
||||
|
||||
|
|
@ -670,7 +670,7 @@ fn m6_7_scrollback_navigation_p99_under_16ms() {
|
|||
const TRIALS: usize = 1000;
|
||||
const P99_THRESHOLD: Duration = Duration::from_millis(16);
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
populate_scrollback(&mut editor, LINES);
|
||||
seek_cursor_to_middle(&mut editor, LINES);
|
||||
|
||||
|
|
@ -749,7 +749,7 @@ fn m6_7_scrollback_search_p99_under_100ms() {
|
|||
const TRIALS: usize = 1000;
|
||||
const P99_THRESHOLD: Duration = Duration::from_millis(100);
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let mut editor = EditorState::new_with_roots(&crate::iso::roots());
|
||||
populate_scrollback(&mut editor, LINES);
|
||||
|
||||
// Warmup: 100 searches at varied positions. Same trace-compilation
|
||||
|
|
@ -815,3 +815,10 @@ fn m6_7_scrollback_search_p99_under_100ms() {
|
|||
p50={p50:?}, p90={p90:?}, max={max:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -89,14 +89,14 @@ fn repl_manifest_declares_exports_and_pmacs_required() {
|
|||
// Bullet 2b: bootstrap loads through the package system
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// EditorState::new() runs the M7.11 bootstrap that materializes
|
||||
// `EditorState::new()` runs the M7.11 bootstrap that materializes
|
||||
// the bundled REPL and pushes its InstalledPackage record into
|
||||
// the roster. After that, `require("repl")` from Lua resolves
|
||||
// through the M7.7 searcher (not the legacy direct eval).
|
||||
|
||||
#[test]
|
||||
fn editor_init_makes_repl_loadable_via_require() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let result: bool = state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -202,3 +202,10 @@ fn materialize_all_produces_one_record_per_bundled_package() {
|
|||
assert!(repl.entry_path().exists());
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ fn outline_package_path() -> PathBuf {
|
|||
fn editor_with_outline() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1262,3 +1262,10 @@ fn outline_aggregate_empty_sources_rejected() {
|
|||
"error must mention non-empty; got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
|
|||
/// Spin up a fresh editor with no file open. The caller drives the
|
||||
/// fs API entirely from Lua chunks via `lua_host.eval`.
|
||||
fn fresh_editor() -> EditorState {
|
||||
EditorState::new()
|
||||
EditorState::new_with_roots(&crate::iso::roots())
|
||||
}
|
||||
|
||||
/// Run a Lua chunk to completion (no `:await`); returns the chunk's
|
||||
|
|
@ -609,3 +609,10 @@ fn fs_watch_reports_file_change_and_can_cancel() {
|
|||
.expect("cancelled");
|
||||
assert!(cancelled, "watch handle must report cancellation");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
|
|||
fn editor_with_dired() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1251,3 +1251,10 @@ fn dired_source_size_under_audit_ceiling() {
|
|||
"M8.2 spec: dired source under 1500 lines; got {lines}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
|
|||
fn editor_with_dired() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -2101,3 +2101,10 @@ fn dired_active_handle_is_cleared_when_active_buffer_removed() {
|
|||
"active_handle must not return a stale handle after its buffer is removed"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F
|
|||
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -913,3 +913,10 @@ fn magit_cursor_on_hidden_descendant_reseats_to_visible_ancestor() {
|
|||
"cursor on hidden A1 must reseat to A's header (line 0)"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ fn pump_until_with_deadline<F: Fn(&EditorState) -> bool>(
|
|||
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -653,3 +653,10 @@ fn magit_build_spec_section_ids_are_stable_canonical_set() {
|
|||
.expect("build_spec ids");
|
||||
assert_eq!(ids, "working,staged,log,branches,stashes");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ fn pump_until_with_deadline<F: Fn(&EditorState) -> bool>(
|
|||
fn editor_with_magit() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1011,3 +1011,10 @@ fn magit_b_c_keybinding_dispatches_to_branch_create_prompt() {
|
|||
.eval(Some("cancel"), "pmacs.minibuffer.cancel()")
|
||||
.expect("cancel");
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ fn outline_package_path() -> PathBuf {
|
|||
fn editor_with_outline() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1090,3 +1090,10 @@ fn outline_parser_unit_parses_org_subset() {
|
|||
.expect("tags");
|
||||
assert_eq!(n_tags, 2);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -725,7 +725,7 @@ fn m9_1_oncrash_policy_does_not_restart_on_clean_exit() {
|
|||
fn m9_1_lua_surface_drives_mcp_lifecycle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_mcp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
let sid_raw: u64 = lua
|
||||
|
|
@ -815,7 +815,7 @@ fn m9_1_lua_surface_drives_mcp_lifecycle() {
|
|||
fn m9_1_lua_send_request_returns_awaitable_handle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_mcp_path();
|
||||
let lua = state.lua_host.lua();
|
||||
|
||||
|
|
@ -924,3 +924,10 @@ fn m9_1_lua_send_request_returns_awaitable_handle() {
|
|||
.load("pmacs.mcp.stop(_G._mcp_test_server)")
|
||||
.exec();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -587,7 +587,7 @@ fn m9_2_cancelled_sibling_wins_over_queued_response() {
|
|||
fn m9_2_lua_read_resource_returns_awaitable_handle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_mcp_path();
|
||||
|
||||
state
|
||||
|
|
@ -738,3 +738,10 @@ fn m9_2_lua_read_resource_returns_awaitable_handle() {
|
|||
.load("pmacs.mcp.stop(_G._mcp_test_server)")
|
||||
.exec();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ fn m9_3_cancellation_reaches_server() {
|
|||
fn m9_3_lua_invoke_tool_returns_awaitable_handle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_mcp_path();
|
||||
|
||||
state
|
||||
|
|
@ -496,3 +496,10 @@ fn m9_3_lua_invoke_tool_returns_awaitable_handle() {
|
|||
.load("pmacs.mcp.stop(_G._mcp_test_server)")
|
||||
.exec();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -317,7 +317,7 @@ fn m9_4_no_args_wire_shape_is_empty_object() {
|
|||
fn m9_4_lua_get_prompt_returns_awaitable_handle() {
|
||||
use pmacs::editor::EditorState;
|
||||
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = fake_mcp_path();
|
||||
|
||||
state
|
||||
|
|
@ -494,3 +494,10 @@ fn m9_4_lua_get_prompt_returns_awaitable_handle() {
|
|||
.load("pmacs.mcp.stop(_G._mcp_test_server)")
|
||||
.exec();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ fn resources_package_path() -> PathBuf {
|
|||
fn editor_with_resources() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1275,3 +1275,10 @@ fn m9_5_stale_buffer_recovers_on_reopen_after_restart() {
|
|||
"after re-open, the buffer must have fresh content; got {reopen_body:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fn tools_package_path() -> PathBuf {
|
|||
fn editor_with_tools() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1532,3 +1532,10 @@ fn m9_6_editor_describe_command_unknown_name_status_only() {
|
|||
"*help* must not be created for an unknown-name describe-command call"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ fn prompts_package_path() -> PathBuf {
|
|||
fn editor_with_prompts() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1386,3 +1386,10 @@ fn m9_7_prompt_hash_includes_required_argument_order() {
|
|||
"reordering required args must change the prompt hash so reconcile re-registers"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ fn prompts_package_path() -> PathBuf {
|
|||
fn editor_with_ai() -> (EditorState, TempDir, TempDir) {
|
||||
let cache = tempfile::tempdir().expect("cache tempdir");
|
||||
let user_root = tempfile::tempdir().expect("user-root tempdir");
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state.lua_host.reopen_init_phase_for_testing();
|
||||
state.lua_host.set_package_install_override(
|
||||
PackageInstallOverride::new()
|
||||
|
|
@ -1122,3 +1122,10 @@ fn m9_8_configured_prompt_missing_on_server_surfaces_error() {
|
|||
state.core.borrow().status
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ fn count_of(kinds: &[String], kind: &str) -> usize {
|
|||
|
||||
#[test]
|
||||
fn switch_away_and_back_reattaches_syntax_overlay_exactly_once() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let path = open_probe_file(&s);
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -86,7 +86,7 @@ fn switch_away_and_back_reattaches_syntax_overlay_exactly_once() {
|
|||
|
||||
#[test]
|
||||
fn panel_quit_restores_overlays_on_the_source_buffer() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let path = open_probe_file(&s);
|
||||
s.lua_host
|
||||
.lua()
|
||||
|
|
@ -121,3 +121,10 @@ fn panel_quit_restores_overlays_on_the_source_buffer() {
|
|||
"leaving a panel restores the source buffer's styling (got {after:?})"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn editor_with_state_dir() -> (EditorState, PathBuf) {
|
|||
SEQ.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).expect("mk state tempdir");
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// Override whatever startup configured with our tempdir.
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
|
||||
|
|
@ -73,12 +73,12 @@ fn state_round_trips_and_rejects_escapes() {
|
|||
|
||||
#[test]
|
||||
fn state_is_inert_when_unconfigured() {
|
||||
// A plain EditorState::new() must NOT configure a state dir — that
|
||||
// A plain `EditorState::new()` must NOT configure a state dir — that
|
||||
// is what keeps the whole integration-test suite (which links the
|
||||
// lib without cfg(test)) from writing to a developer's real
|
||||
// ~/.local/state/pmacs. Only the real entry points call
|
||||
// install_state_dirs(); tests construct EditorState directly.
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
assert!(
|
||||
s.lua_host.lua().app_data_ref::<StateDir>().is_none(),
|
||||
"new() must leave the state dir unconfigured"
|
||||
|
|
@ -182,3 +182,10 @@ fn saveplace_can_be_disabled() {
|
|||
assert_eq!(cursor, 0, "disabled saveplace leaves the cursor at the top");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ fn start_query_replace(s: &mut EditorState, from: &str, to: &str, regex: bool) {
|
|||
|
||||
#[test]
|
||||
fn replace_skip_and_quit_is_selective() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "x x x x");
|
||||
// Cursor to buffer start so all four are ahead of point.
|
||||
goto_start(&s);
|
||||
|
|
@ -107,7 +107,7 @@ fn replace_skip_and_quit_is_selective() {
|
|||
|
||||
#[test]
|
||||
fn bang_replaces_all_remaining() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "b", false);
|
||||
|
|
@ -119,7 +119,7 @@ fn bang_replaces_all_remaining() {
|
|||
|
||||
#[test]
|
||||
fn dot_replaces_current_then_quits() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "z", false);
|
||||
|
|
@ -133,7 +133,7 @@ fn dot_replaces_current_then_quits() {
|
|||
fn growing_replacement_does_not_loop() {
|
||||
// a → aa must not re-match the inserted text (offset-shift + the
|
||||
// search-forward-past-replacement rule).
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "aa", false);
|
||||
|
|
@ -144,7 +144,7 @@ fn growing_replacement_does_not_loop() {
|
|||
|
||||
#[test]
|
||||
fn empty_to_deletes() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a-b-c");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "-", "", false);
|
||||
|
|
@ -155,7 +155,7 @@ fn empty_to_deletes() {
|
|||
|
||||
#[test]
|
||||
fn regex_query_replace_via_binding() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a1 b2 c3");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "[0-9]", "#", true);
|
||||
|
|
@ -167,7 +167,7 @@ fn regex_query_replace_via_binding() {
|
|||
#[test]
|
||||
fn m_percent_binding_starts_query_replace() {
|
||||
// The literal chord: M-% (Alt + Shift+5 → Char('%') with ALT).
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "cat cat");
|
||||
goto_start(&s);
|
||||
s.dispatch_key(
|
||||
|
|
@ -196,7 +196,7 @@ fn m_percent_binding_starts_query_replace() {
|
|||
fn c_m_percent_binding_starts_regexp_query_replace() {
|
||||
// Control-meta-shifted punctuation — the chord most likely to parse
|
||||
// differently across key paths (the C-c H lesson).
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "x1 x2");
|
||||
goto_start(&s);
|
||||
s.dispatch_key(
|
||||
|
|
@ -224,7 +224,7 @@ fn c_m_percent_binding_starts_regexp_query_replace() {
|
|||
|
||||
#[test]
|
||||
fn nothing_matched_leaves_buffer_untouched() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "hello");
|
||||
start_query_replace(&mut s, "zzz", "q", false);
|
||||
let (text, active) = probe(&s);
|
||||
|
|
@ -236,7 +236,7 @@ fn nothing_matched_leaves_buffer_untouched() {
|
|||
fn query_replace_flips_dispatch_idle_so_gpu_round_trips() {
|
||||
// While the interactive phase runs, dispatch_idle must be false so a
|
||||
// semantic frontend round-trips y/n/etc. instead of self-inserting.
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "b", false);
|
||||
|
|
@ -251,7 +251,7 @@ fn query_replace_flips_dispatch_idle_so_gpu_round_trips() {
|
|||
#[test]
|
||||
fn replace_fires_after_edit_hook() {
|
||||
// The Q#QR1 hook: an LSP/syntax observer must see replaced text.
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
|
|
@ -289,7 +289,7 @@ fn bang_fires_after_edit_hook_once_for_the_batch() {
|
|||
// Q#QR1: `!` applies many replacements under one keypress, but the
|
||||
// debounced didChange wants a single after-edit — the shadow
|
||||
// compares revision once across the whole handler.
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
|
|
@ -322,7 +322,7 @@ fn bang_fires_after_edit_hook_once_for_the_batch() {
|
|||
fn quit_via_ret_and_esc_keeps_replacements() {
|
||||
// Q#QR10: RET and Esc both quit (keeping replacements), not just q.
|
||||
for quit in [KeyCode::Enter, KeyCode::Esc] {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "b", false);
|
||||
|
|
@ -337,7 +337,7 @@ fn quit_via_ret_and_esc_keeps_replacements() {
|
|||
#[test]
|
||||
fn ctrl_g_quits_keeping_replacements() {
|
||||
// Q#QR10: C-g exits and KEEPS replacements (unlike isearch's C-g).
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "b", false);
|
||||
|
|
@ -353,7 +353,7 @@ fn ctrl_g_quits_keeping_replacements() {
|
|||
|
||||
#[test]
|
||||
fn del_key_skips_like_n() {
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "a a a");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "a", "b", false);
|
||||
|
|
@ -373,7 +373,7 @@ fn focus_drift_mid_session_aborts_without_touching_either_buffer() {
|
|||
// (simulated by switch_buffer, which the pointer path also uses)
|
||||
// while query-replace is active. The next y must abort, not apply
|
||||
// the origin-buffer match to the now-active unrelated buffer.
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
type_str(&mut s, "foo foo");
|
||||
goto_start(&s);
|
||||
start_query_replace(&mut s, "foo", "bar", false);
|
||||
|
|
@ -416,3 +416,10 @@ fn focus_drift_mid_session_aborts_without_touching_either_buffer() {
|
|||
"origin buffer untouched by the aborted replace"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ impl Fixture {
|
|||
}
|
||||
|
||||
fn editor() -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// No language server may spawn from these fixtures. The LSP rows
|
||||
// that DO want one configure it explicitly.
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
|
|
@ -2043,3 +2043,10 @@ fn a_subscriber_reconciliation_failure_is_reported_and_the_rest_still_reconcile(
|
|||
unreconciled — the raise has to come after the loop, not inside it"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ fn save_refuses_to_clobber_a_file_changed_on_disk() {
|
|||
write(&f, "original\n");
|
||||
let fs = f.display().to_string();
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_and_dirty(&s, &fs);
|
||||
|
||||
// Another writer lands between our read and our save.
|
||||
|
|
@ -88,7 +88,7 @@ fn the_save_command_does_not_fire_after_save_when_it_refuses() {
|
|||
write(&f, "original\n");
|
||||
let fs = f.display().to_string();
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_and_dirty(&s, &fs);
|
||||
exec(
|
||||
&s,
|
||||
|
|
@ -110,7 +110,7 @@ fn save_anyway_overwrites_deliberately_and_resyncs_meta() {
|
|||
write(&f, "original\n");
|
||||
let fs = f.display().to_string();
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_and_dirty(&s, &fs);
|
||||
write(&f, "theirs\n");
|
||||
assert!(!eval::<bool>(&s, "return pmacs.editor.save()"));
|
||||
|
|
@ -137,7 +137,7 @@ fn an_unchanged_file_saves_normally_and_repeatedly() {
|
|||
write(&f, "original\n");
|
||||
let fs = f.display().to_string();
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_and_dirty(&s, &fs);
|
||||
assert!(eval::<bool>(&s, "return pmacs.editor.save()"));
|
||||
assert_eq!(read(&f), "mine original\n");
|
||||
|
|
@ -157,7 +157,7 @@ fn a_deleted_file_is_recreated_not_refused() {
|
|||
write(&f, "original\n");
|
||||
let fs = f.display().to_string();
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
open_and_dirty(&s, &fs);
|
||||
// Nothing on disk to clobber, so recreating it is not data loss.
|
||||
std::fs::remove_file(&f).unwrap();
|
||||
|
|
@ -174,7 +174,7 @@ fn a_new_file_buffer_refuses_once_someone_else_creates_the_file() {
|
|||
let dir = tempdir();
|
||||
let missing = dir.join("draft.txt");
|
||||
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// The argv `[new file]` shape: a path with nothing on disk, no meta.
|
||||
exec(
|
||||
&s,
|
||||
|
|
@ -200,3 +200,10 @@ fn a_new_file_buffer_refuses_once_someone_else_creates_the_file() {
|
|||
assert_eq!(read(&missing), "my draft");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ use pmacs::statusline::{
|
|||
StatuslineEvaluationOutcome, StatuslineEvaluationTarget, evaluate_statusline,
|
||||
};
|
||||
|
||||
#[cfg(feature = "crdt")]
|
||||
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
|
||||
// build, not only the CRDT one.
|
||||
mod common;
|
||||
|
||||
fn exec(state: &EditorState, source: &str) {
|
||||
|
|
@ -41,7 +42,7 @@ fn eval<T: mlua::FromLuaMulti>(state: &EditorState, source: &str) -> T {
|
|||
}
|
||||
|
||||
fn editor() -> EditorState {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
state
|
||||
}
|
||||
|
|
@ -689,7 +690,7 @@ fn a09_11_full_tui_frame_composes_unicode_clips_and_preserves_echo() {
|
|||
// map and polls the real tracker without a buffer edit.
|
||||
#[test]
|
||||
fn a12_builtin_lsp_provider_tracks_real_attachment_and_unknown_label() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let fake = env!("CARGO_BIN_EXE_pmacs_fake_lsp");
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("statusline.rs");
|
||||
|
|
@ -1048,3 +1049,11 @@ fn a16_26_real_daemon_v17_gate_v18_first_frame_and_late_join() {
|
|||
"late join sees established state"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ fn escape_was_armed(state: &mut EditorState, buffer: pmacs::buffer::BufferId, pr
|
|||
/// Acceptance 1: a profile spec is strict, and rejects before anything spawns.
|
||||
#[test]
|
||||
fn acc1_profile_specs_are_strict_and_reject_before_spawning() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let before = state.core.borrow().registry.borrow().ids().len();
|
||||
|
||||
exec(
|
||||
|
|
@ -218,7 +218,7 @@ fn acc1_profile_specs_are_strict_and_reject_before_spawning() {
|
|||
/// Acceptance 2: an unknown profile names the known ones and creates nothing.
|
||||
#[test]
|
||||
fn acc2_unknown_profile_lists_known_names_and_creates_nothing() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -262,7 +262,7 @@ fn acc2_unknown_profile_lists_known_names_and_creates_nothing() {
|
|||
/// unknown-profile path, replacing the exact error being asked for.
|
||||
#[test]
|
||||
fn acc2_malformed_profile_keys_do_not_mask_the_unknown_profile_error() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -297,7 +297,7 @@ fn acc2_malformed_profile_keys_do_not_mask_the_unknown_profile_error() {
|
|||
/// `env` MERGES rather than replacing.
|
||||
#[test]
|
||||
fn acc3_field_resolution_order_and_env_merge() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&state,
|
||||
r#"
|
||||
|
|
@ -323,7 +323,7 @@ fn acc3_field_resolution_order_and_env_merge() {
|
|||
/// Acceptance 3 (explicit command wins) and 4 (`""` means no profile).
|
||||
#[test]
|
||||
fn acc3_acc4_explicit_command_wins_and_empty_default_means_no_profile() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -373,7 +373,7 @@ pmacs.terminal.profiles.fill = {
|
|||
/// fallback) deleted outright.
|
||||
#[test]
|
||||
fn acc5_scrollback_setting_reaches_retained_history() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, FILL_PROFILE);
|
||||
|
||||
// Arm 1: `0` is legal, and means the early rows are GONE.
|
||||
|
|
@ -423,7 +423,7 @@ fn acc5_scrollback_setting_reaches_retained_history() {
|
|||
/// values, and `0` is inside it rather than a disabled sentinel.
|
||||
#[test]
|
||||
fn acc5_scrollback_bounds() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, r#"pmacs.config.set("terminal.scrollback-rows", 0)"#);
|
||||
assert_eq!(
|
||||
state
|
||||
|
|
@ -458,7 +458,7 @@ fn acc5_scrollback_bounds() {
|
|||
/// THAT chord to the child, and an ordinary `C-c` still reaches the child.
|
||||
#[test]
|
||||
fn acc6_acc9_configured_escape_chord_and_literal_repeat() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
let buffer = open_cat_terminal(&state, r#"profile = "echo""#);
|
||||
assert!(tick_until(&mut state, "READY", buffer));
|
||||
|
|
@ -499,7 +499,7 @@ fn acc6_acc9_configured_escape_chord_and_literal_repeat() {
|
|||
/// count that does not grow, and a cache that dies with its terminal.
|
||||
#[test]
|
||||
fn acc7_acc8_acc8a_per_terminal_escape_cache_identity_and_lifecycle() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
let a = open_cat_terminal(&state, r#"profile = "echo""#);
|
||||
exec(&state, "TERM_A = TERM_BUF");
|
||||
|
|
@ -633,7 +633,7 @@ fn acc7_acc8_acc8a_per_terminal_escape_cache_identity_and_lifecycle() {
|
|||
/// the status line, and reports once per terminal per effective bad value.
|
||||
#[test]
|
||||
fn acc10_acc10a_invalid_escape_falls_back_and_reports_once() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, CAT_PROFILE);
|
||||
let buffer = open_cat_terminal(&state, r#"profile = "echo""#);
|
||||
assert!(tick_until(&mut state, "READY", buffer));
|
||||
|
|
@ -699,7 +699,7 @@ fn acc10_acc10a_invalid_escape_falls_back_and_reports_once() {
|
|||
/// proves the second half).
|
||||
#[test]
|
||||
fn acc11_terminal_opening_binding_is_bound_and_shadowed_nothing() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let command: Option<String> = state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -717,7 +717,7 @@ fn acc11_terminal_opening_binding_is_bound_and_shadowed_nothing() {
|
|||
/// defaults reproduce the pre-arc behavior.
|
||||
#[test]
|
||||
fn acc12_defaults_reproduce_prior_behavior() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let lua = state.lua_host.lua();
|
||||
assert_eq!(
|
||||
lua.load(r#"return pmacs.config.get("terminal.default-profile")"#)
|
||||
|
|
@ -744,3 +744,10 @@ fn acc12_defaults_reproduce_prior_behavior() {
|
|||
"no profiles are registered by default"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ fn buffer_count(state: &EditorState) -> usize {
|
|||
/// `copy_selection_bytes` itself.
|
||||
#[test]
|
||||
fn acc13_snapshot_is_the_whole_retained_range_through_the_shared_serializer() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ fn acc13_snapshot_is_the_whole_retained_range_through_the_shared_serializer() {
|
|||
/// buffer-shaped consumer work and what removes the transport arm.
|
||||
#[test]
|
||||
fn acc14_the_snapshot_is_an_ordinary_non_terminal_buffer() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -272,7 +272,7 @@ fn acc14_the_snapshot_is_an_ordinary_non_terminal_buffer() {
|
|||
/// with no change to `src/search.rs` (B1).
|
||||
#[test]
|
||||
fn acc15_isearch_finds_content_only_in_scrollback() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
|
||||
|
|
@ -319,7 +319,7 @@ fn acc15_isearch_finds_content_only_in_scrollback() {
|
|||
/// agreement; what stops the mutation is this.
|
||||
#[test]
|
||||
fn acc16_dispatch_idle_is_false_while_the_snapshot_is_focused() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -344,7 +344,7 @@ fn acc16_dispatch_idle_is_false_while_the_snapshot_is_focused() {
|
|||
/// protection does not depend on which key or command was used.
|
||||
#[test]
|
||||
fn acc16b_the_snapshot_is_immutable_at_the_rope_not_merely_intercepted() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -390,7 +390,7 @@ fn acc16b_the_snapshot_is_immutable_at_the_rope_not_merely_intercepted() {
|
|||
/// leaves the buffer emptiable. Only rope-level `read_only` closes both.
|
||||
#[test]
|
||||
fn acc16c_undo_cannot_empty_the_snapshot_by_chord_or_by_command() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -523,7 +523,7 @@ fn grid_row(cells: &[pmacs::cell::Cell], row: u32, cols: u32) -> String {
|
|||
/// any other owner that adopts the primitive later.
|
||||
#[test]
|
||||
fn acc16d_a_generated_write_notifies_the_window_that_displays_it() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&state,
|
||||
r"
|
||||
|
|
@ -567,7 +567,7 @@ fn acc16d_a_generated_write_notifies_the_window_that_displays_it() {
|
|||
#[cfg(feature = "crdt")]
|
||||
#[test]
|
||||
fn acc16e_a_refresh_queues_the_owners_write_for_replica_mirrors() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -606,7 +606,7 @@ fn acc16e_a_refresh_queues_the_owners_write_for_replica_mirrors() {
|
|||
/// both directions.
|
||||
#[test]
|
||||
fn acc18_reinvoke_refreshes_in_place_and_lifecycle_runs_both_ways() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
|
||||
|
|
@ -681,7 +681,7 @@ fn acc18_reinvoke_refreshes_in_place_and_lifecycle_runs_both_ways() {
|
|||
/// `q` returns to the source terminal.
|
||||
#[test]
|
||||
fn acc19_escape_c_t_enters_copy_mode_and_g_and_q_work() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
let terminal_name = active_buffer_name(&state);
|
||||
|
|
@ -749,7 +749,7 @@ fn acc19_escape_c_t_enters_copy_mode_and_g_and_q_work() {
|
|||
/// tail.
|
||||
#[test]
|
||||
fn acc20_live_terminal_keys_are_unchanged_while_a_snapshot_exists() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
let key = focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -806,7 +806,7 @@ fn acc20_live_terminal_keys_are_unchanged_while_a_snapshot_exists() {
|
|||
/// (or nothing) while the keys behaved differently.
|
||||
#[test]
|
||||
fn acc21_describe_key_reports_the_truth_for_the_snapshot_bindings() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
exec(&state, "pmacs.terminal.copy_mode(TERM_BUF)");
|
||||
|
|
@ -847,7 +847,7 @@ fn acc21_describe_key_reports_the_truth_for_the_snapshot_bindings() {
|
|||
/// (dired's F7 rule); a taken name gets a `<2>` variant instead.
|
||||
#[test]
|
||||
fn acc18a_a_foreign_same_named_buffer_is_never_adopted_or_clobbered() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let terminal = open_fill_terminal(&mut state);
|
||||
focus_terminal(&state, terminal);
|
||||
|
||||
|
|
@ -898,7 +898,7 @@ fn acc18a_a_foreign_same_named_buffer_is_never_adopted_or_clobbered() {
|
|||
/// terminal, and killing either one removes the shared snapshot.
|
||||
#[test]
|
||||
fn acc18b_two_same_named_terminals_get_two_independent_snapshots() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&state, FILL_PROFILE);
|
||||
|
||||
let before = terminal_buffers(&state);
|
||||
|
|
@ -985,7 +985,7 @@ fn acc18b_two_same_named_terminals_get_two_independent_snapshots() {
|
|||
/// snapshot of nothing.
|
||||
#[test]
|
||||
fn copy_mode_refuses_a_non_terminal_buffer() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let err = eval_err(&state, "return pmacs.terminal.copy_mode()");
|
||||
assert!(
|
||||
err.contains("not a terminal"),
|
||||
|
|
@ -1010,7 +1010,7 @@ fn copy_mode_refuses_a_non_terminal_buffer() {
|
|||
/// state. Falsify by deleting the `win.cursor > len` clamp.
|
||||
#[test]
|
||||
fn acc16f_a_shrinking_generated_write_clamps_the_window_cursor() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&state,
|
||||
r"
|
||||
|
|
@ -1066,7 +1066,7 @@ fn acc16f_a_shrinking_generated_write_clamps_the_window_cursor() {
|
|||
/// the measurement.
|
||||
#[test]
|
||||
fn acc16g_a_line_collapsing_generated_write_clamps_view_top() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&state,
|
||||
r"
|
||||
|
|
@ -1129,7 +1129,7 @@ fn acc16g_a_line_collapsing_generated_write_clamps_view_top() {
|
|||
/// `notify_buffer_edit`; the first copy reaches the stale-anchor panic.
|
||||
#[test]
|
||||
fn acc16h_a_shrinking_generated_write_clamps_or_clears_the_selection() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(
|
||||
&state,
|
||||
r"
|
||||
|
|
@ -1224,7 +1224,7 @@ fn acc16h_a_shrinking_generated_write_clamps_or_clears_the_selection() {
|
|||
/// `acc16h` stays green.
|
||||
#[test]
|
||||
fn acc16i_a_shrinking_view_rebuild_clamps_or_clears_the_selection() {
|
||||
let state = EditorState::new();
|
||||
let state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
// 286 bytes, then 154: a real shrink through the help renderer.
|
||||
exec(
|
||||
&state,
|
||||
|
|
@ -1300,3 +1300,10 @@ fn acc16i_a_shrinking_view_rebuild_clamps_or_clears_the_selection() {
|
|||
"the collapsed region is not retained as active-but-empty"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ use pmacs::protocol::{ByteRange, FrontendId, InstanceMessage, ThemeFace};
|
|||
use pmacs::semantic_render::SemanticRenderState;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(feature = "crdt")]
|
||||
// Ungated: `common::iso` (isolated bootstrap roots) is needed in every
|
||||
// build, not only the CRDT one.
|
||||
mod common;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -78,7 +79,7 @@ fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
|
|||
|
||||
/// Fresh editor with LSP spawning disabled.
|
||||
fn editor() -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
s
|
||||
}
|
||||
|
|
@ -184,7 +185,7 @@ fn current_tree_language(state: &EditorState) -> Option<String> {
|
|||
|
||||
/// Open `path` and pump until its parse settles (highlights attached).
|
||||
fn open_and_wait_for_parse(path: std::path::PathBuf) -> EditorState {
|
||||
let mut state = EditorState::open(path).expect("open file");
|
||||
let mut state = EditorState::open_with_roots(path, &crate::iso::roots()).expect("open file");
|
||||
exec(&state, "pmacs.lsp.config = {}");
|
||||
pump_async(&mut state, |s| current_tree_language(s).is_some());
|
||||
state
|
||||
|
|
@ -1511,3 +1512,11 @@ fn minibuffer_and_candidate_faces_apply_through_m_x() {
|
|||
panic!("no candidate suffix rendered: {text:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ fn status(s: &EditorState) -> String {
|
|||
/// Fresh scratch-buffer editor, cursor at 0. Scratch pairing uses the
|
||||
/// `default` set, so `(` pairs — which is what 46c reads.
|
||||
fn editor_with(body: &str) -> EditorState {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
if !body.is_empty() {
|
||||
exec(&s, &format!("pmacs.window.buffer():insert(0, {body:?})"));
|
||||
}
|
||||
|
|
@ -656,7 +656,7 @@ fn a_chain_consumers_edit_reaches_the_first_did_change() {
|
|||
let sink_disp = sink.display().to_string();
|
||||
let fake = fake_lsp_path();
|
||||
|
||||
let mut s = EditorState::new();
|
||||
let mut s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.lua_host.lua().remove_app_data::<StateDir>();
|
||||
s.lua_host.lua().set_app_data(StateDir(dir.clone()));
|
||||
exec(&s, "pmacs.lsp.config = {}");
|
||||
|
|
@ -733,3 +733,10 @@ fn a_chain_consumers_edit_reaches_the_first_did_change() {
|
|||
before lsp.lua's synchronous flush (Q#AP7)"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ fn terminal_cells_reject_child_control_characters() {
|
|||
|
||||
#[test]
|
||||
fn spawn_failure_is_transactional() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let buffers_before = state.core.borrow().registry.borrow().len();
|
||||
let processes_before = state.process_supervisor.borrow().ids().count();
|
||||
state.process_supervisor.borrow_mut().shutdown();
|
||||
|
|
@ -77,7 +77,7 @@ fn spawn_failure_is_transactional() {
|
|||
|
||||
#[test]
|
||||
fn strict_owned_spec_rejects_before_spawn_and_is_mutation_independent() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let buffers_before = state.core.borrow().registry.borrow().len();
|
||||
let mut invalid = TerminalSpec::new("/bin/sh");
|
||||
invalid.rows = 0;
|
||||
|
|
@ -193,7 +193,7 @@ fn read_only_empty_crdt_bootstrap_is_immutable_against_remote_content() {
|
|||
|
||||
#[test]
|
||||
fn final_output_precedes_exact_nonzero_annotation_and_buffer_is_retained() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let mut spec = TerminalSpec::new("/bin/sh");
|
||||
spec.args = vec![
|
||||
"-c".into(),
|
||||
|
|
@ -306,7 +306,7 @@ fn normal_and_signal_annotations_use_exact_pid_and_outcome() {
|
|||
"exited abnormally with signal SIGTERM",
|
||||
),
|
||||
] {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let mut spec = TerminalSpec::new("/bin/sh");
|
||||
spec.args = vec!["-c".into(), script.into()];
|
||||
spec.rows = 6;
|
||||
|
|
@ -334,7 +334,7 @@ fn normal_and_signal_annotations_use_exact_pid_and_outcome() {
|
|||
|
||||
#[test]
|
||||
fn killing_terminal_buffer_prunes_session_and_reaps_owned_process() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let mut spec = TerminalSpec::new("/bin/sh");
|
||||
spec.args = vec!["-c".into(), "sleep 30".into()];
|
||||
let buffer_id = state.open_terminal(spec).expect("open terminal");
|
||||
|
|
@ -364,7 +364,7 @@ fn killing_terminal_buffer_prunes_session_and_reaps_owned_process() {
|
|||
#[test]
|
||||
fn editor_shutdown_kills_term_ignoring_terminal_child() {
|
||||
let pid = {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.process_supervisor
|
||||
.borrow_mut()
|
||||
|
|
@ -397,7 +397,7 @@ fn editor_shutdown_kills_term_ignoring_terminal_child() {
|
|||
|
||||
#[test]
|
||||
fn terminal_tick_does_not_take_non_terminal_process_events() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let mut process = pmacs::process::ProcessSpec::new("ordinary", "/bin/sh");
|
||||
process.args = vec!["-c".into(), "printf ordinary".into()];
|
||||
let ordinary_id = state
|
||||
|
|
@ -424,3 +424,10 @@ fn terminal_tick_does_not_take_non_terminal_process_events() {
|
|||
"TerminalManager must not steal ordinary process output"
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ fn snapshot_text(snapshot: &pmacs::terminal::TerminalSnapshot) -> String {
|
|||
reason = "cross-surface Lua transaction scenario"
|
||||
)]
|
||||
fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let command_lua = lua_string("/bin/sh");
|
||||
let baseline_buffer_id = state.core.borrow().active_buffer_id();
|
||||
|
||||
|
|
@ -364,7 +364,7 @@ fn lua_surface_is_strict_fresh_transactional_and_context_safe() {
|
|||
#[test]
|
||||
#[allow(clippy::too_many_lines, reason = "shared view and controller scenario")]
|
||||
fn shared_screen_keeps_view_scroll_selection_and_controller_independent() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let mut spec = TerminalSpec::new("/bin/sh");
|
||||
spec.args = vec![
|
||||
"-c".into(),
|
||||
|
|
@ -508,7 +508,7 @@ fn terminal_escape_gates_local_bindings_and_double_escape_sends_interrupt() {
|
|||
ready_path.to_str().expect("UTF-8 ready path"),
|
||||
input_path.to_str().expect("UTF-8 input path")
|
||||
);
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
state
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
@ -986,3 +986,11 @@ fn longest_rendered_prefix_separates_absent_child_text_from_a_split_render() {
|
|||
needle.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ fn one_frame(messages: &[InstanceMessage]) -> TerminalFrame {
|
|||
reason = "one producer-baseline lifecycle scenario"
|
||||
)]
|
||||
fn a30_first_frame_is_authoritative_then_only_real_changes_emit() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let frontend_id = FrontendId(31);
|
||||
let terminal_buffer = open_terminal(
|
||||
&mut state,
|
||||
|
|
@ -314,7 +314,7 @@ fn a30_first_frame_is_authoritative_then_only_real_changes_emit() {
|
|||
reason = "one shared-session two-frontend scenario"
|
||||
)]
|
||||
fn a31_two_semantic_frontends_share_one_session_with_independent_views() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let first_id = FrontendId(41);
|
||||
let second_id = FrontendId(42);
|
||||
let terminal_buffer = open_terminal(
|
||||
|
|
@ -459,7 +459,7 @@ fn a31_two_semantic_frontends_share_one_session_with_independent_views() {
|
|||
reason = "one case per rejected identity or bound"
|
||||
)]
|
||||
fn a32_forged_stale_and_out_of_bounds_terminal_events_change_nothing() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let owner = FrontendId(51);
|
||||
let attacker = FrontendId(52);
|
||||
let terminal_buffer = open_terminal(&mut state, "sleep 30", 6, 20);
|
||||
|
|
@ -978,7 +978,7 @@ fn terminal_mode_keeps_reporting_presence_so_peers_drop_the_stale_caret() {
|
|||
/// gesture still claims; only motion does not.
|
||||
#[test]
|
||||
fn hover_does_not_steal_terminal_control_from_the_active_frontend() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let owner = FrontendId(71);
|
||||
let bystander = FrontendId(72);
|
||||
let terminal_buffer = open_terminal(&mut state, "sleep 30", 6, 20);
|
||||
|
|
@ -1075,7 +1075,7 @@ fn hover_does_not_steal_terminal_control_from_the_active_frontend() {
|
|||
/// the empty identity buffer.
|
||||
#[test]
|
||||
fn a28_a30_a_v18_semantic_peer_has_no_terminal_surface() {
|
||||
let mut state = EditorState::new();
|
||||
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||
let frontend_id = FrontendId(61);
|
||||
let terminal_buffer = open_terminal(&mut state, "sleep 30", 4, 20);
|
||||
tick_until(&mut state, Duration::from_secs(5), |state| {
|
||||
|
|
@ -1341,3 +1341,11 @@ fn gpu_terminal_input_reaches_the_child_and_returns_in_a_frame() {
|
|||
report()
|
||||
);
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root. Re-exported rather than re-declared
|
||||
// with `#[path]` — this file already pulls in `common`, and loading one
|
||||
// source file as two modules is `clippy::duplicate_mod`.
|
||||
use common::iso;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fn live_threads() -> usize {
|
|||
fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
|
||||
let baseline = live_threads();
|
||||
for _ in 0..3 {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
drop(s);
|
||||
}
|
||||
// Signal-only shutdown: parked workers exit within their 100ms
|
||||
|
|
@ -45,7 +45,7 @@ fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
|
|||
|
||||
// Idempotence: explicit shutdown twice, then drop runs it a
|
||||
// third time --- none may hang or panic.
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.async_runtime.shutdown_workers();
|
||||
s.async_runtime.shutdown_workers();
|
||||
drop(s);
|
||||
|
|
@ -55,8 +55,15 @@ fn editor_state_drop_releases_workers_and_shutdown_is_idempotent() {
|
|||
#[cfg(not(target_os = "linux"))]
|
||||
#[test]
|
||||
fn explicit_shutdown_is_idempotent() {
|
||||
let s = EditorState::new();
|
||||
let s = EditorState::new_with_roots(&crate::iso::roots());
|
||||
s.async_runtime.shutdown_workers();
|
||||
s.async_runtime.shutdown_workers(); // second call must not hang or panic
|
||||
drop(s); // drop runs shutdown a third time
|
||||
}
|
||||
|
||||
// Isolated bootstrap storage roots (see the module docs): an
|
||||
// integration test is compiled without `cfg(test)`, so a raw
|
||||
// `EditorState::new()` would read the developer's real `init.lua` and
|
||||
// write into their real data root.
|
||||
#[path = "common/iso.rs"]
|
||||
mod iso;
|
||||
|
|
|
|||
Loading…
Reference in New Issue