1254 lines
42 KiB
Rust
1254 lines
42 KiB
Rust
// tests/m8_2_acceptance.rs --- T M8.2 dired-class acceptance.
|
|
|
|
//! Acceptance for the M8.2 deliverable: a Lua-only directory-view
|
|
//! package, installed via `install_local`, exercising the dev-loop
|
|
//! infrastructure shipped in M8.1 end-to-end.
|
|
//!
|
|
//! The package source-of-record for this test lives in-tree at
|
|
//! `tests/fixtures/pmacs-dired/`. A development copy also exists
|
|
//! alongside this repo at `~/Repos/util/pmacs-dired/`, but the
|
|
//! fixture is canonical: the test must remain reproducible from a
|
|
//! fresh clone of *this* repo with no sibling checkouts.
|
|
//!
|
|
//! The fixture is structurally external (separate manifest, loaded
|
|
//! via `install_local`); only its on-disk location is internal.
|
|
//! That preserves the M8 acceptance claim that dired-class fits
|
|
//! the buffer-and-views universality story without core support.
|
|
//!
|
|
//! Spec acceptance bullets being pinned:
|
|
//!
|
|
//! * Opening a directory of 10000 entries renders within 200 ms.
|
|
//! * Subdirectories navigate with Enter; parent with Backspace.
|
|
//! * Sort modes switch via commands, regenerate the buffer
|
|
//! deterministically.
|
|
|
|
use std::path::PathBuf;
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
|
|
use pmacs::editor::EditorState;
|
|
use pmacs::frontend::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
use pmacs::lua_bindings::PackageInstallOverride;
|
|
use pmacs::protocol::FrontendId;
|
|
use tempfile::TempDir;
|
|
|
|
/// Construct a synthetic keyboard event for tests that exercise
|
|
/// the editor's key-dispatch path. Mirrors the helper inside
|
|
/// `editor.rs`'s test module.
|
|
fn plain_key(code: KeyCode) -> KeyEvent {
|
|
KeyEvent {
|
|
code,
|
|
modifiers: KeyModifiers::NONE,
|
|
kind: KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE,
|
|
}
|
|
}
|
|
|
|
/// Resolve the on-disk path to the M8.2 dired package source.
|
|
/// Points at the in-tree fixture so the test is reproducible from
|
|
/// a fresh clone with no sibling checkouts.
|
|
fn dired_package_path() -> PathBuf {
|
|
let here = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
here.join("tests").join("fixtures").join("pmacs-dired")
|
|
}
|
|
|
|
/// Pump the editor's async tick until `predicate` returns true or
|
|
/// the deadline fires. Mirrors the helper in `m4`/`m8_1` — the
|
|
/// dired package's `open()` flow goes through `pmacs.async(...)`
|
|
/// → `pmacs.fs.read_dir(...):await()`, which only settles on a
|
|
/// `tick_async()` cycle.
|
|
fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F) {
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while !predicate(state) {
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"async pump deadline exceeded after 5s"
|
|
);
|
|
state.tick_async();
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
}
|
|
}
|
|
|
|
/// Spin up an editor with package install overrides pointing at a
|
|
/// fresh temp dir. The dired package is then `install_local`-ed
|
|
/// from its sibling source path.
|
|
///
|
|
/// `EditorState::new()` sets the init-complete flag during startup
|
|
/// (the integration-test build doesn't get the `cfg(test)` guard
|
|
/// that lib tests do), so we reopen the init phase before
|
|
/// `install_local`. The `reopen_init_phase_for_testing` helper is
|
|
/// the documented test escape hatch.
|
|
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();
|
|
state.lua_host.reopen_init_phase_for_testing();
|
|
state.lua_host.set_package_install_override(
|
|
PackageInstallOverride::new()
|
|
.with_cache_dir(cache.path().to_path_buf())
|
|
.with_user_install_root(user_root.path().to_path_buf()),
|
|
);
|
|
let pkg = dired_package_path();
|
|
let pkg_str = pkg.display().to_string();
|
|
let install = format!(
|
|
r#"
|
|
pmacs.packages.install_local("{pkg_str}")
|
|
require("pmacs-dired")
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("dired-install"), &install)
|
|
.unwrap_or_else(|e| panic!("install_local + require failed: {e}"));
|
|
(state, cache, user_root)
|
|
}
|
|
|
|
/// Read the active buffer's full text.
|
|
fn active_buffer_text(state: &mut EditorState) -> String {
|
|
state
|
|
.lua_host
|
|
.lua()
|
|
.load(
|
|
r"
|
|
local buf = pmacs.window.buffer()
|
|
return buf:slice(0, buf:len())
|
|
",
|
|
)
|
|
.eval::<String>()
|
|
.expect("read active buffer")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- header + per-entry shape
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dired_open_renders_header_and_one_line_per_entry() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("a.txt"), b"hello").expect("write a");
|
|
std::fs::write(dir.path().join("b.txt"), b"world!").expect("write b");
|
|
std::fs::create_dir(dir.path().join("subdir")).expect("mkdir");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.M8_2_DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.M8_2_DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open"), &open)
|
|
.unwrap_or_else(|e| panic!("dired.open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("M8_2_DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let text = active_buffer_text(&mut state);
|
|
let lines: Vec<&str> = text.lines().collect();
|
|
// 1 header + 3 entries.
|
|
assert_eq!(
|
|
lines.len(),
|
|
4,
|
|
"expected 1 header + 3 entries, got {} lines: {text}",
|
|
lines.len()
|
|
);
|
|
|
|
// Header is the path with trailing colon.
|
|
assert_eq!(lines[0], format!("{path_str}:"));
|
|
|
|
// Each entry line starts with a kind char then 9 perm chars
|
|
// then a space. Subdir lines start with 'd', files with '-'.
|
|
let mut saw_subdir = false;
|
|
let mut saw_a = false;
|
|
let mut saw_b = false;
|
|
for line in &lines[1..] {
|
|
assert!(line.len() > 11, "entry line too short: {line:?}");
|
|
if line.contains(" subdir") {
|
|
assert!(
|
|
line.starts_with('d'),
|
|
"subdir line should start with 'd': {line}"
|
|
);
|
|
saw_subdir = true;
|
|
}
|
|
if line.contains(" a.txt") {
|
|
assert!(
|
|
line.starts_with('-'),
|
|
"a.txt line should start with '-': {line}"
|
|
);
|
|
assert!(line.contains(" 5 "), "size 5 should appear: {line}");
|
|
saw_a = true;
|
|
}
|
|
if line.contains(" b.txt") {
|
|
assert!(
|
|
line.starts_with('-'),
|
|
"b.txt line should start with '-': {line}"
|
|
);
|
|
assert!(line.contains(" 6 "), "size 6 should appear: {line}");
|
|
saw_b = true;
|
|
}
|
|
}
|
|
assert!(
|
|
saw_subdir && saw_a && saw_b,
|
|
"all three entries must render"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- 10K entries render under 200 ms
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
#[cfg_attr(
|
|
target_os = "macos",
|
|
ignore = "hosted macOS debug runners do not consistently satisfy this timing gate"
|
|
)]
|
|
fn dired_open_renders_10k_entries_under_200ms() {
|
|
// Build a directory of 10K small files. The fixture creation
|
|
// itself isn't fast (10K syscalls), so we measure only the
|
|
// dired open() call, not setup.
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
for i in 0..10_000 {
|
|
std::fs::write(dir.path().join(format!("f{i:05}")), b"").expect("write fixture entry");
|
|
}
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.M8_2_DONE = nil
|
|
_G.M8_2_OPEN_START = nil
|
|
_G.M8_2_OPEN_END = nil
|
|
pmacs.async(function()
|
|
_G.M8_2_OPEN_START = os.clock()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.M8_2_OPEN_END = os.clock()
|
|
_G.M8_2_DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open10k"), &open)
|
|
.unwrap_or_else(|e| panic!("dired.open(10K) failed: {e}"));
|
|
|
|
let outer_start = Instant::now();
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("M8_2_DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
let outer_elapsed = outer_start.elapsed();
|
|
|
|
// The pump-loop measurement includes our 2ms sleeps between
|
|
// tick_async cycles. The async-runtime budget for the
|
|
// read_dir reply is bounded by the worker pool's dispatch
|
|
// latency plus the actual lstat-loop cost. For the spec's
|
|
// 200ms ceiling we use the wall-clock from open() to pump
|
|
// settle, which the inner os.clock() reading also captures
|
|
// for cross-checking.
|
|
assert!(
|
|
outer_elapsed < Duration::from_millis(200),
|
|
"M8.2 spec: 10K entries must render within 200ms; took {outer_elapsed:?}"
|
|
);
|
|
|
|
// Sanity: the buffer has 1 header + 10000 entry lines.
|
|
let text = active_buffer_text(&mut state);
|
|
let line_count = text.lines().count();
|
|
assert_eq!(
|
|
line_count, 10_001,
|
|
"10K entries + 1 header = 10001 lines; got {line_count}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- sort modes regenerate the buffer deterministically
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dired_sort_commands_regenerate_buffer_deterministically() {
|
|
// Three files with deliberately disjoint name/size ordering so
|
|
// each sort mode produces a distinct expected order. mtime is
|
|
// hard to control deterministically without sleep; we rely on
|
|
// the name + size cases.
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("a"), b"1234567890").expect("a"); // 10 bytes
|
|
std::fs::write(dir.path().join("b"), b"x").expect("b"); // 1 byte
|
|
std::fs::write(dir.path().join("c"), b"yy").expect("c"); // 2 bytes
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
// Open and verify default sort (by name).
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("sort-open"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let by_name_order = entry_filenames_in_buffer(&mut state);
|
|
assert_eq!(
|
|
by_name_order,
|
|
vec!["a", "b", "c"],
|
|
"default sort should be by name ascending"
|
|
);
|
|
|
|
// Sort by size (largest first): a (10), c (2), b (1).
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("sort-size"),
|
|
r#"pmacs.command.invoke("pmacs-dired.sort-size")"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("sort-size command failed: {e}"));
|
|
let by_size_order = entry_filenames_in_buffer(&mut state);
|
|
assert_eq!(
|
|
by_size_order,
|
|
vec!["a", "c", "b"],
|
|
"sort-size should produce largest first"
|
|
);
|
|
|
|
// Re-sort by name and verify we're back to alphabetical.
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("sort-name"),
|
|
r#"pmacs.command.invoke("pmacs-dired.sort-name")"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("sort-name command failed: {e}"));
|
|
let by_name_again = entry_filenames_in_buffer(&mut state);
|
|
assert_eq!(
|
|
by_name_again,
|
|
vec!["a", "b", "c"],
|
|
"re-sort by name should be deterministic"
|
|
);
|
|
}
|
|
|
|
/// Extract entry filenames in display order from the active dired
|
|
/// buffer. Skips the header (line 0) and pulls the trailing token
|
|
/// of each entry line (filename) for comparison.
|
|
fn entry_filenames_in_buffer(state: &mut EditorState) -> Vec<String> {
|
|
let text = active_buffer_text(state);
|
|
text.lines()
|
|
.skip(1)
|
|
.filter_map(|line| line.split_whitespace().last().map(str::to_string))
|
|
.collect()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- parent-directory navigation rebuilds the listing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dired_parent_command_navigates_up_one_level() {
|
|
// Open dired on a subdirectory, run the parent command, verify
|
|
// the buffer now lists the parent's contents.
|
|
let root = tempfile::tempdir().expect("tempdir");
|
|
let child = root.path().join("child");
|
|
std::fs::create_dir(&child).expect("mkdir child");
|
|
std::fs::write(child.join("inner.txt"), b"i").expect("write inner");
|
|
std::fs::write(root.path().join("sibling.txt"), b"s").expect("write sibling");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let child_str = child.display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{child_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-child"), &open)
|
|
.unwrap_or_else(|e| panic!("open child failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Sanity: buffer shows child's contents.
|
|
assert!(
|
|
active_buffer_text(&mut state).contains("inner.txt"),
|
|
"child's listing should contain inner.txt"
|
|
);
|
|
|
|
// Run the parent command. It dispatches another async work,
|
|
// so we pump again.
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("parent"),
|
|
r#"
|
|
_G.DONE2 = nil
|
|
pmacs.command.invoke("pmacs-dired.parent")
|
|
pmacs.async(function()
|
|
-- Yield a tick so the parent-command's nested
|
|
-- pmacs.async has time to settle. We rely on
|
|
-- pmacs.workers.sleep as the canonical "yield
|
|
-- one tick" helper.
|
|
pmacs.workers.sleep(20):await()
|
|
_G.DONE2 = true
|
|
end)
|
|
"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("parent command failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE2")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let after = active_buffer_text(&mut state);
|
|
let root_str = root.path().display().to_string();
|
|
assert!(
|
|
after.starts_with(&format!("{root_str}:")),
|
|
"after parent, header should be the root path; got: {}",
|
|
after.lines().next().unwrap_or("")
|
|
);
|
|
assert!(
|
|
after.contains("sibling.txt"),
|
|
"parent listing should include sibling.txt"
|
|
);
|
|
assert!(
|
|
after.contains(" child"),
|
|
"parent listing should include the original child as an entry"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- Enter on a subdirectory navigates into it
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Pins the spec bullet "Subdirectories navigate with Enter". The
|
|
// header advertises this acceptance, and the open-line command
|
|
// already implemented it; this test exercises the path end-to-end
|
|
// (cursor positioning + command invocation + buffer rebuild).
|
|
|
|
#[test]
|
|
fn dired_open_line_navigates_into_subdirectory_under_cursor() {
|
|
// Parent has exactly one entry --- the subdirectory --- so we
|
|
// know cursor line 1 is the subdir without depending on sort
|
|
// semantics or file listing order.
|
|
let root = tempfile::tempdir().expect("tempdir");
|
|
let only = root.path().join("only_subdir");
|
|
std::fs::create_dir(&only).expect("mkdir only_subdir");
|
|
std::fs::write(only.join("inside.txt"), b"present").expect("write inside");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let root_str = root.path().display().to_string();
|
|
|
|
// Open dired on the parent.
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{root_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-parent"), &open)
|
|
.unwrap_or_else(|e| panic!("open parent failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Sanity: parent listing has only_subdir on line 1 (line 0 is
|
|
// the header).
|
|
let before = active_buffer_text(&mut state);
|
|
let lines: Vec<&str> = before.lines().collect();
|
|
assert_eq!(
|
|
lines.len(),
|
|
2,
|
|
"parent should have 1 header + 1 entry; got {}: {before}",
|
|
lines.len()
|
|
);
|
|
assert!(
|
|
lines[1].contains("only_subdir"),
|
|
"entry line 1 must be only_subdir: {}",
|
|
lines[1]
|
|
);
|
|
|
|
// Position the cursor on entry line 1, then invoke open-line.
|
|
// The pmacs.editor primitives are `move_line_start` + a loop of
|
|
// `move_down`. We move to top via repeated move_up, then exactly
|
|
// one move_down to land on line 1.
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("enter-on-subdir"),
|
|
r#"
|
|
while pmacs.editor.cursor_line() > 0 do
|
|
pmacs.editor.move_up()
|
|
end
|
|
pmacs.editor.move_line_start()
|
|
pmacs.editor.move_down()
|
|
_G.DONE2 = nil
|
|
pmacs.command.invoke("pmacs-dired.open-line")
|
|
pmacs.async(function()
|
|
-- The open-line dir branch dispatches another
|
|
-- pmacs.async; yield once so its read_dir reply
|
|
-- settles before we read the buffer.
|
|
pmacs.workers.sleep(20):await()
|
|
_G.DONE2 = true
|
|
end)
|
|
"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("enter-on-subdir failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE2")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Buffer must now show the subdirectory's listing.
|
|
let after = active_buffer_text(&mut state);
|
|
let only_str = only.display().to_string();
|
|
assert!(
|
|
after.starts_with(&format!("{only_str}:")),
|
|
"after Enter on subdir, header should be the subdir path; got: {}",
|
|
after.lines().next().unwrap_or("")
|
|
);
|
|
assert!(
|
|
after.contains("inside.txt"),
|
|
"subdir listing should include inside.txt; got: {after}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- Enter on a file errors with a clear message (stub posture)
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The v0.1 dired package can't open files directly --- there's no
|
|
// buffer-from-file primitive yet --- so the open-line command for
|
|
// non-dir entries must error explicitly rather than silently no-op.
|
|
// Pins the stub-posture contract: error names the workaround.
|
|
|
|
#[test]
|
|
fn dired_open_line_on_file_errors_with_actionable_message() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("a_file.txt"), b"content").expect("write file");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-files"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Position the cursor on entry line 1 and invoke. open-line
|
|
// should raise (caught via pcall here so we can inspect the
|
|
// message).
|
|
let msg: String = state
|
|
.lua_host
|
|
.lua()
|
|
.load(
|
|
r#"
|
|
while pmacs.editor.cursor_line() > 0 do
|
|
pmacs.editor.move_up()
|
|
end
|
|
pmacs.editor.move_line_start()
|
|
pmacs.editor.move_down()
|
|
local ok, err = pcall(function()
|
|
pmacs.command.invoke("pmacs-dired.open-line")
|
|
end)
|
|
assert(not ok, "open-line on file should have errored")
|
|
return tostring(err)
|
|
"#,
|
|
)
|
|
.eval()
|
|
.expect("file-branch error eval");
|
|
assert!(
|
|
msg.contains("buffer-from-file"),
|
|
"error must name the missing capability: {msg}"
|
|
);
|
|
assert!(
|
|
msg.contains("C-x C-f") || msg.contains("file-open"),
|
|
"error must point at a workaround: {msg}"
|
|
);
|
|
assert!(
|
|
msg.contains("a_file.txt"),
|
|
"error must include the offending path: {msg}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- relative paths to dired.open are rejected up front
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dired_open_rejects_relative_paths() {
|
|
let (state, _c, _u) = editor_with_dired();
|
|
let msg: String = state
|
|
.lua_host
|
|
.lua()
|
|
.load(
|
|
r#"
|
|
local ok, err = pcall(function()
|
|
require("pmacs-dired").open("./relative")
|
|
end)
|
|
assert(not ok, "relative path should reject")
|
|
return tostring(err)
|
|
"#,
|
|
)
|
|
.eval()
|
|
.expect("relative-path eval");
|
|
assert!(
|
|
msg.contains("absolute"),
|
|
"error must call out the absolute requirement: {msg}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- newlines in filenames don't break the line-per-entry invariant
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// POSIX filenames may contain `\n` and `\r`. If render_entry passed
|
|
// such names through verbatim the buffer's one-line-per-entry
|
|
// contract --- which the wdired layer (M8.3) and cursor-line ->
|
|
// entry mapping both depend on --- would silently break. Verifies
|
|
// that `escape_displayable` keeps each entry on a single line.
|
|
|
|
#[test]
|
|
fn dired_escapes_newlines_in_filenames() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
// Create a file with a literal newline in its name. POSIX
|
|
// permits this; ext4 happily accepts it.
|
|
let nasty_name = "evil\nname.txt";
|
|
std::fs::write(dir.path().join(nasty_name), b"x").expect("write nasty");
|
|
std::fs::write(dir.path().join("normal.txt"), b"y").expect("write normal");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-nasty"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let text = active_buffer_text(&mut state);
|
|
let line_count = text.lines().count();
|
|
// 1 header + 2 entries = 3 lines. Without escaping, the nasty
|
|
// entry would render as two physical lines, giving 4.
|
|
assert_eq!(
|
|
line_count, 3,
|
|
"newline-in-name must not split into multiple buffer lines; got {line_count} lines:\n{text}"
|
|
);
|
|
assert!(
|
|
text.contains("evil\\nname.txt"),
|
|
"newline must render as the escaped sequence \\n: {text}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- escape_displayable disambiguates literal `\` from real newlines
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Wdired (M8.3) will parse rendered entry text to detect line
|
|
// edits, so the rendering must be a one-to-one encoding: a
|
|
// filename containing the literal two-character sequence `\` `n`
|
|
// must not look identical to one containing an actual newline.
|
|
// We escape literal backslashes first, then the control chars,
|
|
// matching `ls --quoting=c`. This test exercises the test seam's
|
|
// `escape_displayable` directly because constructing a filename
|
|
// containing a literal backslash-n in a portable way and reading
|
|
// it back through pmacs.fs is brittle (some filesystems normalize
|
|
// surrogate pairs); the function-level test is the load-bearing
|
|
// guarantee.
|
|
|
|
#[test]
|
|
fn dired_escape_displayable_round_trip_is_unambiguous() {
|
|
let (state, _c, _u) = editor_with_dired();
|
|
let result: mlua::Table = state
|
|
.lua_host
|
|
.lua()
|
|
.load(
|
|
r#"
|
|
local esc = require("pmacs-dired")._test.escape_displayable
|
|
return {
|
|
-- Real newline -> two-char escape "\n"
|
|
real_newline = esc("a\nb"),
|
|
-- Literal "\n" (two chars) -> "\\n" (escaped)
|
|
literal_backslash_n = esc("a\\nb"),
|
|
-- Real CR
|
|
real_cr = esc("a\rb"),
|
|
-- Other C0 controls use visible hex escapes
|
|
c0_soh = esc("a" .. string.char(1) .. "b"),
|
|
c0_us = esc("a" .. string.char(31) .. "b"),
|
|
-- Literal "\\" -> "\\\\"
|
|
only_backslash = esc("\\"),
|
|
-- ASCII passes through
|
|
ascii_only = esc("plain.txt"),
|
|
}
|
|
"#,
|
|
)
|
|
.eval()
|
|
.expect("escape eval");
|
|
|
|
let real_newline: String = result.get("real_newline").unwrap();
|
|
let literal_bs_n: String = result.get("literal_backslash_n").unwrap();
|
|
let real_cr: String = result.get("real_cr").unwrap();
|
|
let c0_soh: String = result.get("c0_soh").unwrap();
|
|
let c0_us: String = result.get("c0_us").unwrap();
|
|
let only_backslash: String = result.get("only_backslash").unwrap();
|
|
let ascii: String = result.get("ascii_only").unwrap();
|
|
|
|
// The whole point: the two encodings must differ.
|
|
assert_ne!(
|
|
real_newline, literal_bs_n,
|
|
"real newline and literal \\n must encode distinctly; both -> {real_newline}"
|
|
);
|
|
assert_eq!(
|
|
real_newline, "a\\nb",
|
|
"real newline -> a\\nb; got {real_newline}"
|
|
);
|
|
assert_eq!(
|
|
literal_bs_n, "a\\\\nb",
|
|
"literal \\n -> a\\\\nb; got {literal_bs_n}"
|
|
);
|
|
assert_eq!(real_cr, "a\\rb");
|
|
assert_eq!(c0_soh, "a\\x01b");
|
|
assert_eq!(c0_us, "a\\x1Fb");
|
|
assert_eq!(only_backslash, "\\\\");
|
|
assert_eq!(ascii, "plain.txt");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- initial open is atomic on read failure
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// `open(path)` must not create an empty dired buffer or switch windows
|
|
// before it knows `path` can be read. Navigation already has the
|
|
// read-then-commit guarantee; initial open needs the same property.
|
|
|
|
#[test]
|
|
fn dired_open_failure_leaves_editor_unchanged() {
|
|
let root = tempfile::tempdir().expect("tempdir");
|
|
let missing = root.path().join("__missing__");
|
|
let missing_str = missing.display().to_string();
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let before_text = active_buffer_text(&mut state);
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.OPEN_FAIL_DONE = nil
|
|
pmacs.async(function()
|
|
local before_buf = pmacs.window.buffer()
|
|
local before_count = #pmacs.buffer.list()
|
|
local ok, err = pcall(function()
|
|
require("pmacs-dired").open("{missing_str}")
|
|
end)
|
|
_G.OPEN_FAIL_OK = ok
|
|
_G.OPEN_FAIL_ERR = tostring(err)
|
|
_G.OPEN_FAIL_BUFFER_UNCHANGED = (pmacs.window.buffer() == before_buf)
|
|
_G.OPEN_FAIL_COUNT_UNCHANGED = (#pmacs.buffer.list() == before_count)
|
|
_G.OPEN_FAIL_DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-failure-atomic"), &open)
|
|
.unwrap_or_else(|e| panic!("open failure eval failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("OPEN_FAIL_DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let ok: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("OPEN_FAIL_OK")
|
|
.expect("OPEN_FAIL_OK");
|
|
assert!(!ok, "open on missing path must fail");
|
|
let buffer_unchanged: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("OPEN_FAIL_BUFFER_UNCHANGED")
|
|
.expect("OPEN_FAIL_BUFFER_UNCHANGED");
|
|
assert!(
|
|
buffer_unchanged,
|
|
"active buffer must not change on failed open"
|
|
);
|
|
let count_unchanged: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("OPEN_FAIL_COUNT_UNCHANGED")
|
|
.expect("OPEN_FAIL_COUNT_UNCHANGED");
|
|
assert!(count_unchanged, "failed open must not create a buffer");
|
|
assert_eq!(
|
|
before_text,
|
|
active_buffer_text(&mut state),
|
|
"failed open must leave active buffer bytes unchanged"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- navigation is atomic on read failure (read-then-commit)
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// If `pmacs.fs.read_dir(target):await()` fails for any reason
|
|
// (path doesn't exist, EACCES, etc.), the dired buffer must
|
|
// remain showing the previous listing rather than landing in a
|
|
// half-updated state where the header points at the new path but
|
|
// the body still shows the old entries. The wdired layer (M8.3)
|
|
// and the cursor-line -> entry mapping both rely on the
|
|
// header-and-body always agreeing.
|
|
|
|
#[test]
|
|
fn dired_navigation_failure_leaves_handle_unchanged() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("alive.txt"), b"present").expect("write alive");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
_G.D_HANDLE = nil
|
|
pmacs.async(function()
|
|
_G.D_HANDLE = require("pmacs-dired").open("{path_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-atomic"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let before = active_buffer_text(&mut state);
|
|
assert!(
|
|
before.contains("alive.txt"),
|
|
"pre-navigation buffer must show alive.txt; got: {before}"
|
|
);
|
|
|
|
// Try to navigate to a path that definitely doesn't exist.
|
|
// Use a temp-tree subpath that hasn't been created --- read_dir
|
|
// returns ENOENT, the await fails with the structured error,
|
|
// and the test seam's navigate_to should leave handle.path /
|
|
// .entries / the buffer unchanged.
|
|
let bogus = format!("{path_str}/__no_such_dir__");
|
|
let nav = format!(
|
|
r#"
|
|
_G.NAV_OK = nil
|
|
_G.NAV_ERR = nil
|
|
_G.NAV_DONE = nil
|
|
local h = _G.D_HANDLE
|
|
local path_before = h.path
|
|
local entries_before = #h.entries
|
|
local navigate_to = require("pmacs-dired")._test.navigate_to
|
|
pmacs.async(function()
|
|
local ok, err = pcall(navigate_to, h, "{bogus}")
|
|
_G.NAV_OK = ok
|
|
_G.NAV_ERR = tostring(err)
|
|
-- After failure, handle must be unchanged.
|
|
_G.NAV_PATH_UNCHANGED = (h.path == path_before)
|
|
_G.NAV_ENTRIES_UNCHANGED = (#h.entries == entries_before)
|
|
_G.NAV_DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("nav-atomic"), &nav)
|
|
.unwrap_or_else(|e| panic!("nav eval failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("NAV_DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
let nav_ok: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("NAV_OK")
|
|
.expect("NAV_OK");
|
|
assert!(
|
|
!nav_ok,
|
|
"navigate_to to non-existent path must error (got success)"
|
|
);
|
|
let path_ok: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("NAV_PATH_UNCHANGED")
|
|
.expect("NAV_PATH_UNCHANGED");
|
|
assert!(
|
|
path_ok,
|
|
"handle.path must be unchanged after failed navigation"
|
|
);
|
|
let entries_ok: bool = state
|
|
.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get("NAV_ENTRIES_UNCHANGED")
|
|
.expect("NAV_ENTRIES_UNCHANGED");
|
|
assert!(
|
|
entries_ok,
|
|
"handle.entries must be unchanged after failed navigation"
|
|
);
|
|
|
|
let after = active_buffer_text(&mut state);
|
|
assert_eq!(
|
|
before, after,
|
|
"buffer must be byte-identical after a failed navigation"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- sort-mtime sorts newest-first
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The pmacs-dired.sort-mtime command was advertised but not
|
|
// directly tested in the first pass. Construct three files with
|
|
// controlled mtimes, invoke the command, verify the rendered
|
|
// order is newest-first.
|
|
|
|
#[test]
|
|
fn dired_sort_mtime_orders_newest_first() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
// Create files in a known order, then explicitly stamp their
|
|
// mtimes to make the test independent of filesystem timestamp
|
|
// resolution / ordering during creation.
|
|
let oldest = dir.path().join("c_oldest");
|
|
let middle = dir.path().join("a_middle");
|
|
let newest = dir.path().join("b_newest");
|
|
for p in [&oldest, &middle, &newest] {
|
|
std::fs::write(p, b"x").expect("write");
|
|
}
|
|
let now = SystemTime::now();
|
|
std::fs::File::options()
|
|
.write(true)
|
|
.open(&oldest)
|
|
.unwrap()
|
|
.set_modified(now - Duration::from_mins(2))
|
|
.expect("set mtime oldest");
|
|
std::fs::File::options()
|
|
.write(true)
|
|
.open(&middle)
|
|
.unwrap()
|
|
.set_modified(now - Duration::from_mins(1))
|
|
.expect("set mtime middle");
|
|
std::fs::File::options()
|
|
.write(true)
|
|
.open(&newest)
|
|
.unwrap()
|
|
.set_modified(now)
|
|
.expect("set mtime newest");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("mtime-open"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("sort-mtime"),
|
|
r#"pmacs.command.invoke("pmacs-dired.sort-mtime")"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("sort-mtime failed: {e}"));
|
|
let order = entry_filenames_in_buffer(&mut state);
|
|
assert_eq!(
|
|
order,
|
|
vec!["b_newest", "a_middle", "c_oldest"],
|
|
"sort-mtime must list newest first"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- RET / Backspace key dispatch invokes the bound commands
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The other navigation tests invoke commands directly via
|
|
// pmacs.command.invoke, which masks regressions in the actual
|
|
// keybinding path (M.open's pmacs.keymap.bind calls). This test
|
|
// dispatches synthetic key events through EditorState::dispatch_key,
|
|
// the same path the running editor uses.
|
|
|
|
#[test]
|
|
fn dired_ret_and_backspace_keybindings_navigate() {
|
|
let root = tempfile::tempdir().expect("tempdir");
|
|
let only = root.path().join("only_subdir");
|
|
std::fs::create_dir(&only).expect("mkdir");
|
|
std::fs::write(only.join("inner.txt"), b"in").expect("write inner");
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
let root_str = root.path().display().to_string();
|
|
let only_str = only.display().to_string();
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{root_str}")
|
|
_G.DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("open-keybind"), &open)
|
|
.unwrap_or_else(|e| panic!("open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
// Position cursor on the entry line (line 1).
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("cursor-to-entry"),
|
|
r"
|
|
while pmacs.editor.cursor_line() > 0 do
|
|
pmacs.editor.move_up()
|
|
end
|
|
pmacs.editor.move_line_start()
|
|
pmacs.editor.move_down()
|
|
",
|
|
)
|
|
.expect("cursor positioning");
|
|
|
|
// Press RET. The keybinding routes to pmacs-dired.open-line,
|
|
// which dispatches a pmacs.async navigation. Wait for the
|
|
// header to flip to the subdir path.
|
|
state.dispatch_key(FrontendId::LOCAL, plain_key(KeyCode::Enter));
|
|
let target_header = format!("{only_str}:");
|
|
pump_until(&mut state, |s| {
|
|
let lua = s.lua_host.lua();
|
|
match lua
|
|
.load(
|
|
r"
|
|
local buf = pmacs.window.buffer()
|
|
return buf:slice(0, buf:len())
|
|
",
|
|
)
|
|
.eval::<String>()
|
|
{
|
|
Ok(text) => text.starts_with(&target_header),
|
|
Err(_) => false,
|
|
}
|
|
});
|
|
assert!(
|
|
active_buffer_text(&mut state).contains("inner.txt"),
|
|
"after RET the subdir's inner.txt should be visible"
|
|
);
|
|
|
|
// Press Backspace. Should navigate back to the root via the
|
|
// pmacs-dired.parent binding.
|
|
state.dispatch_key(FrontendId::LOCAL, plain_key(KeyCode::Backspace));
|
|
let root_header = format!("{root_str}:");
|
|
pump_until(&mut state, |s| {
|
|
let lua = s.lua_host.lua();
|
|
match lua
|
|
.load(
|
|
r"
|
|
local buf = pmacs.window.buffer()
|
|
return buf:slice(0, buf:len())
|
|
",
|
|
)
|
|
.eval::<String>()
|
|
{
|
|
Ok(text) => text.starts_with(&root_header),
|
|
Err(_) => false,
|
|
}
|
|
});
|
|
assert!(
|
|
active_buffer_text(&mut state).contains("only_subdir"),
|
|
"after Backspace the root's only_subdir entry should be visible"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- pmacs-dired itself reloads post-init
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// The package claims reload safety, and the runtime supports command
|
|
// unregistering from post-init unload hooks. Pin the actual fixture so
|
|
// a future package-local cleanup regression can't hide behind the
|
|
// generic M8.1d lifecycle tests.
|
|
|
|
#[test]
|
|
fn dired_package_reload_is_safe_after_init_complete() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("after_reload.txt"), b"ok").expect("write");
|
|
let path_str = dir.path().display().to_string();
|
|
|
|
let (mut state, _c, _u) = editor_with_dired();
|
|
state.lua_host.set_init_complete();
|
|
|
|
state
|
|
.lua_host
|
|
.eval(
|
|
Some("reload-dired"),
|
|
r#"
|
|
pmacs.packages.reload("pmacs-dired")
|
|
assert(pmacs.command.exists("pmacs-dired.open-line"))
|
|
assert(pmacs.command.exists("pmacs-dired.parent"))
|
|
assert(pmacs.command.exists("pmacs-dired.sort-name"))
|
|
assert(pmacs.command.exists("pmacs-dired.sort-mtime"))
|
|
assert(pmacs.command.exists("pmacs-dired.sort-size"))
|
|
"#,
|
|
)
|
|
.unwrap_or_else(|e| panic!("pmacs-dired reload failed: {e}"));
|
|
|
|
let open = format!(
|
|
r#"
|
|
_G.RELOAD_OPEN_DONE = nil
|
|
pmacs.async(function()
|
|
require("pmacs-dired").open("{path_str}")
|
|
_G.RELOAD_OPEN_DONE = true
|
|
end)
|
|
"#
|
|
);
|
|
state
|
|
.lua_host
|
|
.eval(Some("reload-dired-open"), &open)
|
|
.unwrap_or_else(|e| panic!("post-reload open failed: {e}"));
|
|
pump_until(&mut state, |s| {
|
|
s.lua_host
|
|
.lua()
|
|
.globals()
|
|
.get::<bool>("RELOAD_OPEN_DONE")
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
assert!(
|
|
active_buffer_text(&mut state).contains("after_reload.txt"),
|
|
"fresh dired buffer should still open after package reload"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T M8.2 --- pmacs-dired source size under 1500 lines
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dired_source_size_under_audit_ceiling() {
|
|
let init_path = dired_package_path().join("init.lua");
|
|
let src = std::fs::read_to_string(&init_path).expect("read init.lua");
|
|
let lines = src.lines().count();
|
|
assert!(
|
|
lines < 1500,
|
|
"M8.2 spec: dired source under 1500 lines; got {lines}"
|
|
);
|
|
}
|