use super::*;
#[cfg(unix)]
#[test]
fn a_name_that_is_not_utf8_still_names_its_file() {
use std::os::unix::ffi::OsStrExt;
let dir = std::env::temp_dir().join(format!("fm84-latin1-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let raw = std::ffi::OsStr::from_bytes(b"caf\xe9.txt");
std::fs::write(dir.join(raw), "contents").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.open_dir(true, dir.clone(), None);
let item = app_state.children_left.iter().find(|item| item.name_full != "..").unwrap().clone();
assert!(!item.name.is_empty());
let index = app_state.children_left.iter().position(|row| row.name_full == item.name_full).unwrap();
app_state.state_left.select(Some(index));
let (path, _, _) = app_state.cursor_target().unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "contents");
assert!(!dir.join(&item.name_full).exists());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_file_called_three_dots_is_not_the_parent_entry() {
let dir = std::env::temp_dir().join(format!("fm84-three-dots-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("..."), "dots").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.open_dir(true, dir.clone(), Some("...".as_ref()));
let item = &app_state.children_left[app_state.state_left.selected().unwrap()];
assert_eq!(item.name, "..");
assert!(!item.is_parent());
assert!(app_state.children_left[0].is_parent());
let (path, _, _) = app_state.cursor_target().unwrap();
assert_eq!(path, dir.join("..."));
app_state.toggle_selection_no_size();
assert!(app_state.selected_left.contains(std::ffi::OsStr::new("...")));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_reread_gathers_the_preview_and_the_detail_again() {
let dir = std::env::temp_dir().join(format!("fm84-reread-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("notes.txt"), "hello").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.show_preview = true;
app_state.open_dir(true, dir.clone(), Some("notes.txt".as_ref()));
app_state.refresh_cursor_detail();
app_state.refresh_preview();
assert_eq!(app_state.cursor_detail.as_ref().unwrap().size, "5 bytes");
assert_eq!(app_state.preview.as_ref().unwrap().lines, ["hello"]);
std::fs::write(dir.join("notes.txt"), "hello, a longer file now").unwrap();
app_state.reload_panel(true, None);
app_state.refresh_cursor_detail();
app_state.refresh_preview();
assert_eq!(app_state.cursor_detail.as_ref().unwrap().size, "24 bytes");
assert_eq!(app_state.preview.as_ref().unwrap().lines, ["hello, a longer file now"]);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn an_unreadable_directory_is_refused_once() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("fm84-unreadable-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let locked = dir.join("locked");
std::fs::create_dir_all(&locked).unwrap();
std::fs::write(dir.join("here.txt"), "").unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::read_dir(&locked).is_ok() {
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
return std::fs::remove_dir_all(&dir).unwrap();
}
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.watch_interval = std::time::Duration::from_millis(5);
app_state.open_dir(true, dir.clone(), None);
let listed = app_state.children_left.len();
app_state.open_dir(true, locked.clone(), None);
assert!(app_state.error.is_some());
assert_eq!(app_state.dir_left, dir);
assert_eq!(app_state.children_left.len(), listed);
app_state.reset_error();
std::thread::sleep(std::time::Duration::from_millis(50));
app_state.poll_watchers();
app_state.refresh_stale_panels();
assert!(app_state.error.is_none());
app_state.open_dir(true, locked.parent().unwrap().to_path_buf(), None);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
app_state.open_dir(true, locked.clone(), None);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
app_state.reload_panel(true, None);
assert!(app_state.error.is_some());
app_state.reset_error();
std::thread::sleep(std::time::Duration::from_millis(50));
app_state.poll_watchers();
app_state.refresh_stale_panels();
assert!(app_state.error.is_none());
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn each_tab_keeps_its_own_way_back() {
let dir = std::env::temp_dir().join(format!("fm84-tab-history-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("a")).unwrap();
std::fs::create_dir_all(dir.join("b")).unwrap();
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.join("a"), None);
app_state.open_dir(true, dir.join("b"), None);
app_state.new_tab();
app_state.travel(true);
assert_eq!(app_state.dir_left, dir.join("b"), "a new tab has nowhere to go back to");
app_state.open_dir(true, dir.clone(), None);
app_state.select_tab(true, 0);
app_state.travel(true);
assert_eq!(app_state.dir_left, dir.join("a"));
app_state.select_tab(true, 1);
app_state.travel(true);
assert_eq!(app_state.dir_left, dir.join("b"));
app_state.travel(true);
assert_eq!(app_state.dir_left, dir.join("b"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn ctrl_arrows_send_the_entry_under_the_cursor_to_a_panel() {
let dir = std::env::temp_dir().join(format!("fm84-open-in-panel-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("inner")).unwrap();
std::fs::write(dir.join("note.txt"), "").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.open_dir(true, dir.clone(), None);
app_state.open_dir(false, std::env::temp_dir(), None);
app_state.is_left_active = true;
let at = |app_state: &AppState, name: &str| app_state.children_left.iter().position(|item| item.name_full == name);
let cursor_right = |app_state: &AppState| app_state.state_right.selected().and_then(|index| app_state.children_right.get(index)).map(|item| item.name_full.clone());
app_state.state_left.select(at(&app_state, "inner"));
app_state.open_in_panel(false);
assert_eq!(app_state.dir_right, dir.join("inner"));
assert!(app_state.is_left_active);
assert_eq!(app_state.dir_left, dir);
app_state.state_left.select(at(&app_state, "note.txt"));
app_state.open_in_panel(false);
assert_eq!(app_state.dir_right, dir);
assert_eq!(cursor_right(&app_state).as_deref(), Some("note.txt"));
app_state.state_left.select(at(&app_state, ".."));
app_state.open_in_panel(false);
assert_eq!(app_state.dir_right, std::env::temp_dir());
assert_eq!(cursor_right(&app_state), dir.file_name().map(|name| name.to_string_lossy().into_owned()));
app_state.state_left.select(at(&app_state, "inner"));
app_state.open_in_panel(true);
assert_eq!(app_state.dir_left, dir.join("inner"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn tabs_keep_where_each_was_left() {
let dir = std::env::temp_dir().join(format!("fm84-tabs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("inner")).unwrap();
for name in ["one.txt", "two.txt"] {
std::fs::write(dir.join(name), "").unwrap();
std::fs::write(dir.join("inner").join(name), "").unwrap();
}
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
let at = |app_state: &AppState, name: &str| app_state.children_left.iter().position(|item| item.name_full == name);
let cursor = |app_state: &AppState| app_state.state_left.selected().and_then(|index| app_state.children_left.get(index)).map(|item| item.name_full.clone());
app_state.selected_left.insert("one.txt".into());
app_state.state_left.select(at(&app_state, "two.txt"));
app_state.new_tab();
assert_eq!((app_state.tabs_left.list.len(), app_state.tabs_left.active), (2, 1));
assert_eq!(app_state.dir_left, dir);
assert!(app_state.selected_left.is_empty());
assert_eq!(cursor(&app_state).as_deref(), Some("two.txt"));
app_state.open_dir(true, dir.join("inner"), None);
app_state.selected_left.insert("two.txt".into());
assert_eq!(app_state.tab_dirs(true), (vec![dir.clone(), dir.join("inner")], 1));
app_state.select_tab(true, 0);
assert_eq!(app_state.dir_left, dir);
assert_eq!(cursor(&app_state).as_deref(), Some("two.txt"));
assert_eq!(app_state.selected_left.iter().collect::<Vec<_>>(), [&std::ffi::OsString::from("one.txt")]);
app_state.cycle_tab(true);
assert_eq!(app_state.dir_left, dir.join("inner"));
assert_eq!(app_state.selected_left.iter().collect::<Vec<_>>(), [&std::ffi::OsString::from("two.txt")]);
app_state.cycle_tab(true);
assert_eq!(app_state.tabs_left.active, 0);
app_state.select_tab(true, 1);
app_state.close_tab(true, 0);
assert_eq!((app_state.tabs_left.list.len(), app_state.tabs_left.active), (1, 0));
assert_eq!(app_state.dir_left, dir.join("inner"));
app_state.close_tab(true, 0);
assert_eq!(app_state.tabs_left.list.len(), 1);
app_state.new_tab();
app_state.open_dir(true, dir.clone(), None);
app_state.close_tab(true, 1);
assert_eq!(app_state.dir_left, dir.join("inner"));
assert_eq!(app_state.tabs_right.list.len(), 1);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn a_tab_that_cannot_be_read_is_refused_and_kept() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("fm84-tab-locked-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let locked = dir.join("locked");
std::fs::create_dir_all(&locked).unwrap();
std::fs::write(dir.join("here.txt"), "").unwrap();
let mut app_state = AppState::new();
app_state.options = Options::default();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
app_state.new_tab();
app_state.open_dir(true, locked.clone(), None);
app_state.select_tab(true, 0);
app_state.selected_left.insert("here.txt".into());
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::read_dir(&locked).is_ok() {
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
return std::fs::remove_dir_all(&dir).unwrap();
}
app_state.select_tab(true, 1);
assert!(app_state.error.is_some());
assert_eq!(app_state.tabs_left.active, 0);
assert_eq!(app_state.dir_left, dir);
assert_eq!(app_state.selected_left.len(), 1);
assert_eq!(app_state.tab_dirs(true), (vec![dir.clone(), locked.clone()], 0));
app_state.reset_error();
app_state.close_tab(true, 0);
assert!(app_state.error.is_some());
assert_eq!(app_state.tab_dirs(true), (vec![dir.clone(), locked.clone()], 0));
assert_eq!(app_state.selected_left.len(), 1);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_slow_directory_leaves_the_panel_as_it_was_until_it_lands() {
let dir = std::env::temp_dir().join(format!("fm84-slow-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("far")).unwrap();
std::fs::write(dir.join("far").join("there.txt"), "").unwrap();
let mut app_state = AppState::new();
app_state.is_left_active = true;
app_state.open_dir(true, dir.clone(), None);
let listed = app_state.children_left.len();
let (sender, answer) = std::sync::mpsc::channel();
app_state.listing_left = Some(crate::app::PendingListing {
dir: dir.join("far"),
navigate: true,
select: None,
tab: None,
travel: crate::app::Travel::Go,
answer,
_stop: Default::default(),
restore_flat: None,
});
assert!(app_state.panel_busy(true));
app_state.new_tab();
assert_eq!(app_state.tabs_left.list.len(), 1);
assert_eq!((app_state.dir_left.clone(), app_state.children_left.len()), (dir.clone(), listed));
app_state.reload_panel(true, None);
assert!(app_state.listing_left.as_ref().is_some_and(|pending| pending.dir == dir.join("far")));
app_state.poll_listings();
assert!(app_state.panel_busy(true), "nothing has landed yet");
sender.send(crate::app::read_listing(&dir.join("far"), &app_state.options, None)).unwrap();
app_state.poll_listings();
assert!(!app_state.panel_busy(true));
assert_eq!(app_state.dir_left, dir.join("far"));
assert!(app_state.children_left.iter().any(|item| item.name_full == "there.txt"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_late_refusal_or_esc_puts_the_tab_back() {
let dir = std::env::temp_dir().join(format!("fm84-late-tab-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut app_state = AppState::new();
app_state.open_dir(true, dir.clone(), None);
app_state.tabs_left = crate::app::Tabs::new(vec![dir.clone(), dir.join("locked")], 0);
for refused in [true, false] {
let (sender, answer) = std::sync::mpsc::channel();
app_state.tabs_left.active = 1;
let tab = crate::app::TabSwitch {
selected: HashSet::new(),
undo: crate::app::TabUndo::Active(0),
};
app_state.listing_left = Some(crate::app::PendingListing {
dir: dir.join("locked"),
navigate: true,
select: None,
tab: Some(tab),
travel: crate::app::Travel::Stay,
answer,
_stop: Default::default(),
restore_flat: None,
});
if refused {
let denied = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
sender
.send(crate::app::Listed {
dir: Some(dir.join("locked")),
items: Err(denied),
branch: None,
stamp: None,
disk: None,
archive: None,
truncated: false,
})
.unwrap();
app_state.poll_listings();
assert!(app_state.error.is_some());
app_state.reset_error();
} else {
app_state.cancel_listing(true);
}
assert!(!app_state.panel_busy(true));
assert_eq!(app_state.tabs_left.active, 0);
assert_eq!(app_state.dir_left, dir);
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn changes_and_details_arrive_from_their_threads() {
let dir = std::env::temp_dir().join(format!("fm84-watched-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("notes.txt"), "hello").unwrap();
let mut app_state = AppState::new();
app_state.watch_interval = std::time::Duration::from_millis(5);
app_state.open_dir(true, dir.clone(), Some("notes.txt".as_ref()));
std::fs::write(dir.join("new.txt"), "").unwrap();
let started = std::time::Instant::now();
while !app_state.children_left.iter().any(|item| item.name_full == "new.txt") {
assert!(started.elapsed() < std::time::Duration::from_secs(10), "the change was never seen");
std::thread::sleep(std::time::Duration::from_millis(5));
app_state.poll_watchers();
app_state.refresh_stale_panels();
}
app_state.detail_wait = std::time::Duration::ZERO;
app_state.show_preview = true;
let at = app_state.children_left.iter().position(|item| item.name_full == "notes.txt");
app_state.state_left.select(at);
app_state.cursor_detail = None;
app_state.refresh_cursor_detail();
assert_eq!(app_state.cursor_detail.as_ref().unwrap().name, "notes.txt");
let started = std::time::Instant::now();
loop {
app_state.refresh_cursor_detail();
app_state.refresh_preview();
let detail = app_state.cursor_detail.as_ref().unwrap();
let preview = app_state.preview.as_ref().unwrap();
if !detail.awaiting && !preview.awaiting {
assert_eq!(detail.size, "5 bytes");
assert_eq!(preview.lines, ["hello"]);
break;
}
assert!(started.elapsed() < std::time::Duration::from_secs(10), "the details never came");
std::thread::sleep(std::time::Duration::from_millis(5));
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_click_lands_on_the_icon_it_looks_like() {
let slots: Vec<(u16, u16)> = (0..7).map(|i| (1 + i * 2, 2)).collect();
assert_eq!(slot_at(&slots, 0), None); assert_eq!(slot_at(&slots, 1), Some(0)); assert_eq!(slot_at(&slots, 2), Some(0)); assert_eq!(slot_at(&slots, 3), Some(1));
assert_eq!(slot_at(&slots, 13), Some(6)); assert_eq!(slot_at(&slots, 14), Some(6));
assert_eq!(slot_at(&slots, 15), None); assert_eq!(slot_at(&slots, 200), None);
for (index, &(start, width)) in slots.iter().enumerate() {
for offset in start..start + width {
assert_eq!(slot_at(&slots, offset), Some(index), "offset {offset}");
}
}
assert_eq!(slot_at(&[], 1), None);
}
#[test]
fn a_slower_older_listing_does_not_land_over_a_newer_one() {
use crate::app::listing::{PendingListing, Travel, read_listing};
let dir = std::env::temp_dir().join(format!("fm84-older-listing-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("old"), "").unwrap();
let mut app_state = AppState::new();
app_state.open_dir(true, dir.clone(), None);
let older = read_listing(&dir, &app_state.options, None);
let (sender, answer) = std::sync::mpsc::channel();
app_state.listing_left = Some(PendingListing {
dir: dir.clone(),
navigate: false,
select: None,
tab: None,
travel: Travel::Stay,
answer,
_stop: Default::default(),
restore_flat: None,
});
std::fs::write(dir.join("new"), "").unwrap();
app_state.reload_panel(true, None);
assert!(app_state.listing_left.is_none(), "the older one is no longer waited for");
let _ = sender.send(older);
app_state.poll_listings();
let names: Vec<&str> = app_state.children_left.iter().map(|item| item.name.as_str()).collect();
assert_eq!(names, ["..", "new", "old"]);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_flat_listing_given_up_on_leaves_the_panel_as_it_was() {
let dir = std::env::temp_dir().join(format!("fm84-flat-cancel-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("sub")).unwrap();
let mut app_state = AppState::new();
app_state.open_dir(true, dir.clone(), None);
app_state.is_left_active = true;
let (_sender, answer) = std::sync::mpsc::channel();
app_state.flat_left = Some(dir.clone());
app_state.listing_left = Some(crate::app::PendingListing {
dir: dir.clone(),
navigate: false,
select: None,
tab: None,
travel: crate::app::Travel::Stay,
answer,
_stop: Default::default(),
restore_flat: Some(None),
});
app_state.cancel_listing(true);
assert!(app_state.flat_left.is_none());
app_state.listing_wait = std::time::Duration::ZERO;
app_state.toggle_flat();
if app_state.panel_busy(true) {
assert_eq!(app_state.listing_left.as_ref().and_then(|pending| pending.restore_flat.clone()), Some(None));
app_state.cancel_listing(true);
assert!(app_state.flat_left.is_none());
}
std::fs::remove_dir_all(&dir).unwrap();
}