From b0a02d30f97d15e0c6fc19e5f4f7b8c56500ff7a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 5 Jun 2019 08:03:49 +0530 Subject: [PATCH] move application tests closer to... the application. Nice! --- Makefile | 2 +- src/interactive/app_test.rs | 327 +++++++++++++++++++++++++++++++++++ src/interactive/mod.rs | 3 + tests/interactive.rs | 329 ------------------------------------ 4 files changed, 331 insertions(+), 330 deletions(-) create mode 100644 src/interactive/app_test.rs delete mode 100644 tests/interactive.rs diff --git a/Makefile b/Makefile index bbc8b735..68d58243 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ benchmark: target/release/dua tests: unit-tests journey-tests unit-tests: - cargo test --test interactive + cargo test --bin dua continuous-unit-tests: watchexec $(MAKE) unit-tests diff --git a/src/interactive/app_test.rs b/src/interactive/app_test.rs new file mode 100644 index 00000000..fe97fe2d --- /dev/null +++ b/src/interactive/app_test.rs @@ -0,0 +1,327 @@ +use crate::interactive::TerminalApp; +use dua::{ + traverse::{EntryData, Tree, TreeIndex}, + ByteFormat, Color, SortMode, TraversalSorting, WalkOptions, +}; +use failure::Error; +use petgraph::prelude::NodeIndex; +use pretty_assertions::assert_eq; +use std::{ffi::OsStr, ffi::OsString, fmt, path::Path, path::PathBuf}; +use termion::input::TermRead; +use tui::{backend::TestBackend, Terminal}; + +const FIXTURE_PATH: &'static str = "tests/fixtures"; + +fn debug(item: impl fmt::Debug) -> String { + format!("{:?}", item) +} + +#[test] +fn it_can_handle_ending_traversal_reaching_top_but_skipping_levels() -> Result<(), Error> { + let (_, app) = initialized_app_and_terminal(&["sample-01"])?; + let expected_tree = sample_01_tree(); + + assert_eq!( + debug(app.traversal.tree), + debug(expected_tree), + "filesystem graph is stable and matches the directory structure" + ); + Ok(()) +} + +#[test] +fn it_can_handle_ending_traversal_without_reaching_the_top() -> Result<(), Error> { + let (_, app) = initialized_app_and_terminal(&["sample-02"])?; + let expected_tree = sample_02_tree(); + + assert_eq!( + debug(app.traversal.tree), + debug(expected_tree), + "filesystem graph is stable and matches the directory structure" + ); + Ok(()) +} + +fn node_by_index(app: &TerminalApp, id: TreeIndex) -> &EntryData { + app.traversal.tree.node_weight(id).unwrap() +} + +fn node_by_name(app: &TerminalApp, name: impl AsRef) -> &EntryData { + node_by_index(app, index_by_name(&app, name)) +} + +fn index_by_name_and_size( + app: &TerminalApp, + name: impl AsRef, + size: Option, +) -> TreeIndex { + let name = name.as_ref(); + let t: Vec<_> = app + .traversal + .tree + .node_indices() + .map(|idx| (idx, node_by_index(app, idx))) + .filter_map(|(idx, e)| { + if e.name == name + && match size { + Some(s) => s == e.size, + None => true, + } + { + Some(idx) + } else { + None + } + }) + .collect(); + match t.len() { + 1 => t[0], + 0 => panic!("Node named '{}' not found in tree", name.to_string_lossy()), + n => panic!("Node named '{}' found {} times", name.to_string_lossy(), n), + } +} + +fn index_by_name(app: &TerminalApp, name: impl AsRef) -> TreeIndex { + index_by_name_and_size(app, name, None) +} + +#[test] +fn simple_user_journey() -> Result<(), Error> { + let long_root = "sample-02/dir"; + let short_root = "sample-01"; + let (mut terminal, mut app) = initialized_app_and_terminal(&[short_root, long_root])?; + + // POST-INIT + // after initialization, we expect that... + { + assert_eq!( + app.state.sorting, + SortMode::SizeDescending, + "it will sort entries in descending order by size" + ); + + let first_selected_path = OsString::from(format!("{}/{}", FIXTURE_PATH, long_root)); + assert_eq!( + node_by_name(&app, &first_selected_path).name, + first_selected_path, + "the roots are always listed with the given (possibly long) names", + ); + + assert_eq!( + node_by_name(&app, fixture_str(short_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "it selects the first node in the list", + ); + + assert_eq!( + app.traversal.root_index, app.state.root, + "the root is the 'virtual' root", + ); + } + + // SORTING + { + // when hitting the S key + app.process_events(&mut terminal, b"s".keys())?; + assert_eq!( + app.state.sorting, + SortMode::SizeAscending, + "it sets the sort mode to ascending by size" + ); + // when hitting the S key again + app.process_events(&mut terminal, b"s".keys())?; + assert_eq!( + app.state.sorting, + SortMode::SizeDescending, + "it sets the sort mode to descending by size" + ); + } + + // Entry-Navigation + { + // when hitting the j key + app.process_events(&mut terminal, b"j".keys())?; + assert_eq!( + node_by_name(&app, fixture_str(long_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "it moves the cursor down and selects the next entry based on the current sort mode" + ); + // when hitting it while there is nowhere to go + app.process_events(&mut terminal, b"j".keys())?; + assert_eq!( + node_by_name(&app, fixture_str(long_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "it stays at the previous position" + ); + // when hitting the k key + app.process_events(&mut terminal, b"k".keys())?; + assert_eq!( + node_by_name(&app, fixture_str(short_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "it moves the cursor up and selects the next entry based on the current sort mode" + ); + // when hitting the k key again + app.process_events(&mut terminal, b"k".keys())?; + assert_eq!( + node_by_name(&app, fixture_str(short_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "it stays at the current cursor position as there is nowhere to go" + ); + // when hitting the o key with a directory selected + app.process_events(&mut terminal, b"o".keys())?; + { + let new_root_idx = index_by_name(&app, fixture_str(short_root)); + assert_eq!( + new_root_idx, app.state.root, + "it enters the entry if it is a directory, changing the root" + ); + assert_eq!( + index_by_name(&app, "dir"), + *app.state.selected.as_ref().unwrap(), + "it selects the first entry in the directory" + ); + + // when hitting the u key while inside a sub-directory + app.process_events(&mut terminal, b"u".keys())?; + { + assert_eq!( + app.traversal.root_index, app.state.root, + "it sets the root to be the (roots) parent directory, being the virtual root" + ); + assert_eq!( + node_by_name(&app, fixture_str(short_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "changes the selection to the first item in the list of entries" + ); + } + } + // when hitting the u key while inside of the root directory + // We are moving the cursor down just to have a non-default selection + app.process_events(&mut terminal, b"ju".keys())?; + { + assert_eq!( + app.traversal.root_index, app.state.root, + "it keeps the root - it can't go further up" + ); + assert_eq!( + node_by_name(&app, fixture_str(long_root)), + node_by_index(&app, *app.state.selected.as_ref().unwrap()), + "keeps the previous selection" + ); + } + } + + Ok(()) +} + +fn fixture(p: impl AsRef) -> PathBuf { + Path::new(FIXTURE_PATH).join(p) +} + +fn fixture_str(p: impl AsRef) -> String { + fixture(p).to_str().unwrap().to_owned() +} + +fn initialized_app_and_terminal( + fixture_paths: &[&str], +) -> Result<(Terminal, TerminalApp), Error> { + let mut terminal = Terminal::new(TestBackend::new(40, 20))?; + std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; + + let input = fixture_paths.iter().map(fixture).collect(); + let app = TerminalApp::initialize( + &mut terminal, + WalkOptions { + threads: 1, + byte_format: ByteFormat::Metric, + color: Color::None, + sorting: TraversalSorting::AlphabeticalByFileName, + }, + input, + )?; + Ok((terminal, app)) +} + +fn sample_01_tree() -> Tree { + let mut t = Tree::new(); + { + let mut add_node = make_add_node(&mut t); + let root_size = 1259070; + let r = add_node("", root_size, None); + { + let s = add_node(&fixture_str("sample-01"), root_size, Some(r)); + { + add_node(".hidden.666", 666, Some(s)); + add_node("a", 256, Some(s)); + add_node("b.empty", 0, Some(s)); + add_node("c.lnk", 1, Some(s)); + let d = add_node("dir", 1258024, Some(s)); + { + add_node("1000bytes", 1000, Some(d)); + add_node("dir-a.1mb", 1_000_000, Some(d)); + add_node("dir-a.kb", 1024, Some(d)); + let e = add_node("empty-dir", 0, Some(d)); + { + add_node(".gitkeep", 0, Some(e)); + } + let sub = add_node("sub", 256_000, Some(d)); + { + add_node("dir-sub-a.256kb", 256_000, Some(sub)); + } + } + add_node("z123.b", 123, Some(s)); + } + } + } + t +} + +fn sample_02_tree() -> Tree { + let mut t = Tree::new(); + { + let mut add_node = make_add_node(&mut t); + let root_size = 1540; + let r = add_node("", root_size, None); + { + let s = add_node( + format!("{}/{}", FIXTURE_PATH, "sample-02").as_str(), + root_size, + Some(r), + ); + { + add_node("a", 256, Some(s)); + add_node("b", 1, Some(s)); + let d = add_node("dir", 1283, Some(s)); + { + add_node("c", 257, Some(d)); + add_node("d", 2, Some(d)); + let e = add_node("empty-dir", 0, Some(d)); + { + add_node(".gitkeep", 0, Some(e)); + } + let sub = add_node("sub", 1024, Some(d)); + { + add_node("e", 1024, Some(sub)); + } + } + } + } + } + t +} + +fn make_add_node<'a>( + t: &'a mut Tree, +) -> impl FnMut(&str, u64, Option) -> NodeIndex + 'a { + move |name, size, maybe_from_idx| { + let n = t.add_node(EntryData { + name: OsString::from(name), + size, + metadata_io_error: false, + }); + if let Some(from) = maybe_from_idx { + t.add_edge(from, n, ()); + } + n + } +} diff --git a/src/interactive/mod.rs b/src/interactive/mod.rs index ae1d34e0..ef02372d 100644 --- a/src/interactive/mod.rs +++ b/src/interactive/mod.rs @@ -2,3 +2,6 @@ mod app; pub mod widgets; pub use self::app::*; + +#[cfg(test)] +mod app_test; diff --git a/tests/interactive.rs b/tests/interactive.rs deleted file mode 100644 index 5210c9b2..00000000 --- a/tests/interactive.rs +++ /dev/null @@ -1,329 +0,0 @@ -mod app { - use dua::{ - interactive::TerminalApp, - traverse::{EntryData, Tree, TreeIndex}, - ByteFormat, Color, SortMode, TraversalSorting, WalkOptions, - }; - use failure::Error; - use petgraph::prelude::NodeIndex; - use pretty_assertions::assert_eq; - use std::{ffi::OsStr, ffi::OsString, fmt, path::Path, path::PathBuf}; - use termion::input::TermRead; - use tui::{backend::TestBackend, Terminal}; - - const FIXTURE_PATH: &'static str = "tests/fixtures"; - - fn debug(item: impl fmt::Debug) -> String { - format!("{:?}", item) - } - - #[test] - fn it_can_handle_ending_traversal_reaching_top_but_skipping_levels() -> Result<(), Error> { - let (_, app) = initialized_app_and_terminal(&["sample-01"])?; - let expected_tree = sample_01_tree(); - - assert_eq!( - debug(app.traversal.tree), - debug(expected_tree), - "filesystem graph is stable and matches the directory structure" - ); - Ok(()) - } - - #[test] - fn it_can_handle_ending_traversal_without_reaching_the_top() -> Result<(), Error> { - let (_, app) = initialized_app_and_terminal(&["sample-02"])?; - let expected_tree = sample_02_tree(); - - assert_eq!( - debug(app.traversal.tree), - debug(expected_tree), - "filesystem graph is stable and matches the directory structure" - ); - Ok(()) - } - - fn node_by_index(app: &TerminalApp, id: TreeIndex) -> &EntryData { - app.traversal.tree.node_weight(id).unwrap() - } - - fn node_by_name(app: &TerminalApp, name: impl AsRef) -> &EntryData { - node_by_index(app, index_by_name(&app, name)) - } - - fn index_by_name_and_size( - app: &TerminalApp, - name: impl AsRef, - size: Option, - ) -> TreeIndex { - let name = name.as_ref(); - let t: Vec<_> = app - .traversal - .tree - .node_indices() - .map(|idx| (idx, node_by_index(app, idx))) - .filter_map(|(idx, e)| { - if e.name == name - && match size { - Some(s) => s == e.size, - None => true, - } - { - Some(idx) - } else { - None - } - }) - .collect(); - match t.len() { - 1 => t[0], - 0 => panic!("Node named '{}' not found in tree", name.to_string_lossy()), - n => panic!("Node named '{}' found {} times", name.to_string_lossy(), n), - } - } - fn index_by_name(app: &TerminalApp, name: impl AsRef) -> TreeIndex { - index_by_name_and_size(app, name, None) - } - - #[test] - fn simple_user_journey() -> Result<(), Error> { - let long_root = "sample-02/dir"; - let short_root = "sample-01"; - let (mut terminal, mut app) = initialized_app_and_terminal(&[short_root, long_root])?; - - // POST-INIT - // after initialization, we expect that... - { - assert_eq!( - app.state.sorting, - SortMode::SizeDescending, - "it will sort entries in descending order by size" - ); - - let first_selected_path = OsString::from(format!("{}/{}", FIXTURE_PATH, long_root)); - assert_eq!( - node_by_name(&app, &first_selected_path).name, - first_selected_path, - "the roots are always listed with the given (possibly long) names", - ); - - assert_eq!( - node_by_name(&app, fixture_str(short_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "it selects the first node in the list", - ); - - assert_eq!( - app.traversal.root_index, app.state.root, - "the root is the 'virtual' root", - ); - } - - // SORTING - { - // when hitting the S key - app.process_events(&mut terminal, b"s".keys())?; - assert_eq!( - app.state.sorting, - SortMode::SizeAscending, - "it sets the sort mode to ascending by size" - ); - // when hitting the S key again - app.process_events(&mut terminal, b"s".keys())?; - assert_eq!( - app.state.sorting, - SortMode::SizeDescending, - "it sets the sort mode to descending by size" - ); - } - - // Entry-Navigation - { - // when hitting the j key - app.process_events(&mut terminal, b"j".keys())?; - assert_eq!( - node_by_name(&app, fixture_str(long_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "it moves the cursor down and selects the next entry based on the current sort mode" - ); - // when hitting it while there is nowhere to go - app.process_events(&mut terminal, b"j".keys())?; - assert_eq!( - node_by_name(&app, fixture_str(long_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "it stays at the previous position" - ); - // when hitting the k key - app.process_events(&mut terminal, b"k".keys())?; - assert_eq!( - node_by_name(&app, fixture_str(short_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "it moves the cursor up and selects the next entry based on the current sort mode" - ); - // when hitting the k key again - app.process_events(&mut terminal, b"k".keys())?; - assert_eq!( - node_by_name(&app, fixture_str(short_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "it stays at the current cursor position as there is nowhere to go" - ); - // when hitting the o key with a directory selected - app.process_events(&mut terminal, b"o".keys())?; - { - let new_root_idx = index_by_name(&app, fixture_str(short_root)); - assert_eq!( - new_root_idx, app.state.root, - "it enters the entry if it is a directory, changing the root" - ); - assert_eq!( - index_by_name(&app, "dir"), - *app.state.selected.as_ref().unwrap(), - "it selects the first entry in the directory" - ); - - // when hitting the u key while inside a sub-directory - app.process_events(&mut terminal, b"u".keys())?; - { - assert_eq!( - app.traversal.root_index, - app.state.root, - "it sets the root to be the (roots) parent directory, being the virtual root" - ); - assert_eq!( - node_by_name(&app, fixture_str(short_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "changes the selection to the first item in the list of entries" - ); - } - } - // when hitting the u key while inside of the root directory - // We are moving the cursor down just to have a non-default selection - app.process_events(&mut terminal, b"ju".keys())?; - { - assert_eq!( - app.traversal.root_index, app.state.root, - "it keeps the root - it can't go further up" - ); - assert_eq!( - node_by_name(&app, fixture_str(long_root)), - node_by_index(&app, *app.state.selected.as_ref().unwrap()), - "keeps the previous selection" - ); - } - } - - Ok(()) - } - - fn fixture(p: impl AsRef) -> PathBuf { - Path::new(FIXTURE_PATH).join(p) - } - - fn fixture_str(p: impl AsRef) -> String { - fixture(p).to_str().unwrap().to_owned() - } - - fn initialized_app_and_terminal( - fixture_paths: &[&str], - ) -> Result<(Terminal, TerminalApp), Error> { - let mut terminal = Terminal::new(TestBackend::new(40, 20))?; - std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?; - - let input = fixture_paths.iter().map(fixture).collect(); - let app = TerminalApp::initialize( - &mut terminal, - WalkOptions { - threads: 1, - byte_format: ByteFormat::Metric, - color: Color::None, - sorting: TraversalSorting::AlphabeticalByFileName, - }, - input, - )?; - Ok((terminal, app)) - } - - fn sample_01_tree() -> Tree { - let mut t = Tree::new(); - { - let mut add_node = make_add_node(&mut t); - let root_size = 1259070; - let r = add_node("", root_size, None); - { - let s = add_node(&fixture_str("sample-01"), root_size, Some(r)); - { - add_node(".hidden.666", 666, Some(s)); - add_node("a", 256, Some(s)); - add_node("b.empty", 0, Some(s)); - add_node("c.lnk", 1, Some(s)); - let d = add_node("dir", 1258024, Some(s)); - { - add_node("1000bytes", 1000, Some(d)); - add_node("dir-a.1mb", 1_000_000, Some(d)); - add_node("dir-a.kb", 1024, Some(d)); - let e = add_node("empty-dir", 0, Some(d)); - { - add_node(".gitkeep", 0, Some(e)); - } - let sub = add_node("sub", 256_000, Some(d)); - { - add_node("dir-sub-a.256kb", 256_000, Some(sub)); - } - } - add_node("z123.b", 123, Some(s)); - } - } - } - t - } - fn sample_02_tree() -> Tree { - let mut t = Tree::new(); - { - let mut add_node = make_add_node(&mut t); - let root_size = 1540; - let r = add_node("", root_size, None); - { - let s = add_node( - format!("{}/{}", FIXTURE_PATH, "sample-02").as_str(), - root_size, - Some(r), - ); - { - add_node("a", 256, Some(s)); - add_node("b", 1, Some(s)); - let d = add_node("dir", 1283, Some(s)); - { - add_node("c", 257, Some(d)); - add_node("d", 2, Some(d)); - let e = add_node("empty-dir", 0, Some(d)); - { - add_node(".gitkeep", 0, Some(e)); - } - let sub = add_node("sub", 1024, Some(d)); - { - add_node("e", 1024, Some(sub)); - } - } - } - } - } - t - } - - fn make_add_node<'a>( - t: &'a mut Tree, - ) -> impl FnMut(&str, u64, Option) -> NodeIndex + 'a { - move |name, size, maybe_from_idx| { - let n = t.add_node(EntryData { - name: OsString::from(name), - size, - metadata_io_error: false, - }); - if let Some(from) = maybe_from_idx { - t.add_edge(from, n, ()); - } - n - } - } - -}