diff --git a/Cargo.lock b/Cargo.lock index 8032b77d..61cceb41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f57fec1ac7e4de72dcc69811795f1a7172ed06012f80a5d1ee651b62484f588" +checksum = "a88b6bd5df287567ffdf4ddf4d33060048e1068308e5f62d81c6f9824a045a48" dependencies = [ "bstr", "doc-comment", @@ -52,6 +52,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bitflags" version = "1.2.1" @@ -69,12 +75,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - [[package]] name = "cfg-if" version = "1.0.0" @@ -107,6 +107,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.5" @@ -136,24 +160,22 @@ dependencies = [ "ansi_term 0.12.1", "assert_cmd", "clap", - "crossbeam-channel", - "ignore", "lscolors", "num_cpus", + "rayon", "stfu8", "tempfile", "terminal_size", "thousands", "unicode-width", - "walkdir", "winapi-util", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "either" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "getrandom" @@ -166,19 +188,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "globset" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - [[package]] name = "hermit-abi" version = "0.1.18" @@ -188,24 +197,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ignore" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" -dependencies = [ - "crossbeam-utils", - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -214,18 +205,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" - -[[package]] -name = "log" -version = "0.4.14" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "lscolors" @@ -242,6 +224,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -252,12 +243,6 @@ dependencies = [ "libc", ] -[[package]] -name = "once_cell" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" - [[package]] name = "ppv-lite86" version = "0.2.10" @@ -292,9 +277,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha", @@ -304,9 +289,9 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -314,27 +299,52 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" dependencies = [ "bitflags", ] @@ -352,12 +362,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -dependencies = [ - "byteorder", -] +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" @@ -375,13 +382,10 @@ dependencies = [ ] [[package]] -name = "same-file" -version = "1.0.6" +name = "scopeguard" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "stfu8" @@ -449,15 +453,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "treeline" version = "0.1.0" @@ -485,17 +480,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b194356c..3afc89c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,7 @@ lscolors = "0.7" num_cpus = "1" terminal_size = "0.1" unicode-width = "0.1" -ignore = "0.4" -crossbeam-channel = "0.5" -walkdir = "2.3" +rayon="1" thousands = "0.2" stfu8 = "0.2" diff --git a/src/dirwalker.rs b/src/dirwalker.rs new file mode 100644 index 00000000..7874ded8 --- /dev/null +++ b/src/dirwalker.rs @@ -0,0 +1,176 @@ +use std::fs; + +use crate::node::Node; +use rayon::iter::ParallelBridge; +use rayon::prelude::ParallelIterator; +use std::path::PathBuf; + +use std::sync::atomic; +use std::sync::atomic::AtomicBool; + +use std::collections::HashSet; + +use crate::node::build_node; +use std::fs::DirEntry; + +pub fn walk_it( + dirs: HashSet, + ignore_directories: HashSet, + use_apparent_size: bool, + by_filecount: bool, + ignore_hidden: bool, +) -> (Vec, bool) { + let permissions_flag = AtomicBool::new(false); + + let top_level_nodes: Vec<_> = dirs + .into_iter() + .filter_map(|d| { + let n = walk( + d, + &permissions_flag, + &ignore_directories, + use_apparent_size, + by_filecount, + ignore_hidden, + ); + match n { + Some(n) => { + let mut inodes: HashSet<(u64, u64)> = HashSet::new(); + clean_inodes(n, &mut inodes, use_apparent_size) + } + None => None, + } + }) + .collect(); + (top_level_nodes, permissions_flag.into_inner()) +} + +// Remove files which have the same inode, we don't want to double count them. +fn clean_inodes( + x: Node, + inodes: &mut HashSet<(u64, u64)>, + use_apparent_size: bool, +) -> Option { + if !use_apparent_size { + if let Some(id) = x.inode_device { + if inodes.contains(&id) { + return None; + } + inodes.insert(id); + } + } + + let new_children: Vec<_> = x + .children + .into_iter() + .filter_map(|c| clean_inodes(c, inodes, use_apparent_size)) + .collect(); + + return Some(Node { + name: x.name, + size: x.size + new_children.iter().map(|c| c.size).sum::(), + children: new_children, + inode_device: x.inode_device, + }); +} + +fn ignore_file( + entry: &DirEntry, + ignore_hidden: bool, + ignore_directories: &HashSet, +) -> bool { + let is_dot_file = entry.file_name().to_str().unwrap_or("").starts_with('.'); + let is_ignored_path = ignore_directories.contains(&entry.path()); + (is_dot_file && ignore_hidden) || is_ignored_path +} + +fn walk( + dir: PathBuf, + permissions_flag: &AtomicBool, + ignore_directories: &HashSet, + use_apparent_size: bool, + by_filecount: bool, + ignore_hidden: bool, +) -> Option { + let mut children = vec![]; + + if let Ok(entries) = fs::read_dir(dir.clone()) { + children = entries + .into_iter() + .par_bridge() + .filter_map(|entry| { + if let Ok(ref entry) = entry { + // uncommenting the below line gives simpler code but + // rayon doesn't parallelise as well giving a 3X performance drop + // hence we unravel the recursion a bit + + // return walk(entry.path(), permissions_flag, ignore_directories, use_apparent_size, by_filecount, ignore_hidden); + + if !ignore_file(&entry, ignore_hidden, &ignore_directories) { + if let Ok(data) = entry.file_type() { + if data.is_dir() && !data.is_symlink() { + return walk( + entry.path(), + permissions_flag, + ignore_directories, + use_apparent_size, + by_filecount, + ignore_hidden, + ); + } + return build_node( + entry.path(), + vec![], + use_apparent_size, + by_filecount, + ); + } + } + } else { + permissions_flag.store(true, atomic::Ordering::Relaxed); + } + None + }) + .collect(); + } else { + permissions_flag.store(true, atomic::Ordering::Relaxed); + } + build_node(dir, children, use_apparent_size, by_filecount) +} + +mod tests { + #[allow(unused_imports)] + use super::*; + + #[cfg(test)] + fn create_node() -> Node { + Node { + name: PathBuf::new(), + size: 10, + children: vec![], + inode_device: Some((5, 6)), + } + } + + #[test] + fn test_should_ignore_file() { + let mut inodes = HashSet::new(); + let n = create_node(); + + // First time we insert the node + assert!(clean_inodes(n.clone(), &mut inodes, false) == Some(n.clone())); + + // Second time is a duplicate - we ignore it + assert!(clean_inodes(n.clone(), &mut inodes, false) == None); + } + + #[test] + fn test_should_not_ignore_files_if_using_apparent_size() { + let mut inodes = HashSet::new(); + let n = create_node(); + + // If using apparent size we include Nodes, even if duplicate inodes + assert!(clean_inodes(n.clone(), &mut inodes, true) == Some(n.clone())); + assert!(clean_inodes(n.clone(), &mut inodes, true) == Some(n.clone())); + } +} diff --git a/src/display.rs b/src/display.rs index d7d64599..57cc0e58 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,6 +1,6 @@ extern crate ansi_term; -use crate::utils::{Errors, Node}; +use crate::display_node::DisplayNode; use self::ansi_term::Colour::Red; use lscolors::{LsColors, Style}; @@ -60,7 +60,7 @@ impl DisplayData { } } - fn percent_size(&self, node: &Node) -> f32 { + fn percent_size(&self, node: &DisplayNode) -> f32 { let result = node.size as f32 / self.base_size as f32; if result.is_normal() { result @@ -83,7 +83,7 @@ impl DrawData<'_> { } // TODO: can we test this? - fn generate_bar(&self, node: &Node, level: usize) -> String { + fn generate_bar(&self, node: &DisplayNode, level: usize) -> String { let chars_in_bar = self.percent_bar.chars().count(); let num_bars = chars_in_bar as f32 * self.display_data.percent_size(node); let mut num_not_my_bar = (chars_in_bar as i32) - num_bars as i32; @@ -107,21 +107,23 @@ impl DrawData<'_> { #[allow(clippy::too_many_arguments)] pub fn draw_it( - errors: Errors, + permission_error: bool, use_full_path: bool, is_reversed: bool, no_colors: bool, no_percents: bool, terminal_width: usize, by_filecount: bool, - root_node: Node, + option_root_node: Option, ) { - if errors.permissions { + if permission_error { eprintln!("Did not have permissions for all directories"); } - if errors.not_found { - eprintln!("Not all directories were found"); + if option_root_node.is_none() { + return; } + let root_node = option_root_node.unwrap(); + let num_chars_needed_on_left_most = if by_filecount { let max_size = root_node.children.iter().map(|n| n.size).fold(0, max); max_size.separate_with_commas().chars().count() @@ -131,11 +133,8 @@ pub fn draw_it( let terminal_width = terminal_width - 9 - num_chars_needed_on_left_most; let num_indent_chars = 3; - let longest_string_length = root_node - .children - .iter() - .map(|c| find_longest_dir_name(&c, num_indent_chars, terminal_width, !use_full_path)) - .fold(0, max); + let longest_string_length = + find_longest_dir_name(&root_node, num_indent_chars, terminal_width, !use_full_path); let max_bar_length = if no_percents || longest_string_length >= terminal_width as usize { 0 @@ -145,27 +144,30 @@ pub fn draw_it( let first_size_bar = repeat(BLOCKS[0]).take(max_bar_length).collect::(); - for c in root_node.get_children_from_node(is_reversed) { - let display_data = DisplayData { - short_paths: !use_full_path, - is_reversed, - colors_on: !no_colors, - by_filecount, - num_chars_needed_on_left_most, - base_size: c.size, - longest_string_length, - ls_colors: LsColors::from_env().unwrap_or_default(), - }; - let draw_data = DrawData { - indent: "".to_string(), - percent_bar: first_size_bar.clone(), - display_data: &display_data, - }; - display_node(c, &draw_data, true, true); - } + let display_data = DisplayData { + short_paths: !use_full_path, + is_reversed, + colors_on: !no_colors, + by_filecount, + num_chars_needed_on_left_most, + base_size: root_node.size, + longest_string_length, + ls_colors: LsColors::from_env().unwrap_or_default(), + }; + let draw_data = DrawData { + indent: "".to_string(), + percent_bar: first_size_bar, + display_data: &display_data, + }; + display_node(root_node, &draw_data, true, true); } -fn find_longest_dir_name(node: &Node, indent: usize, terminal: usize, long_paths: bool) -> usize { +fn find_longest_dir_name( + node: &DisplayNode, + indent: usize, + terminal: usize, + long_paths: bool, +) -> usize { let printable_name = get_printable_name(&node.name, long_paths); let longest = min( UnicodeWidthStr::width(&*printable_name) + 1 + indent, @@ -179,7 +181,7 @@ fn find_longest_dir_name(node: &Node, indent: usize, terminal: usize, long_paths .fold(longest, max) } -fn display_node(node: Node, draw_data: &DrawData, is_biggest: bool, is_last: bool) { +fn display_node(node: DisplayNode, draw_data: &DrawData, is_biggest: bool, is_last: bool) { // hacky way of working out how deep we are in the tree let indent = draw_data.get_new_indent(!node.children.is_empty(), is_last); let level = ((indent.chars().count() - 1) / 2) - 1; @@ -254,11 +256,13 @@ fn get_printable_name>(dir_name: &P, long_paths: bool) -> String encode_u8(printable_name.display().to_string().as_bytes()) } -fn pad_or_trim_filename(node: &Node, indent: &str, display_data: &DisplayData) -> String { +fn pad_or_trim_filename(node: &DisplayNode, indent: &str, display_data: &DisplayData) -> String { let name = get_printable_name(&node.name, display_data.short_paths); let indent_and_name = format!("{} {}", indent, name); let width = UnicodeWidthStr::width(&*indent_and_name); + assert!(display_data.longest_string_length >= width); + // Add spaces after the filename so we can draw the % used bar chart. let name_and_padding = name + &(repeat(" ") @@ -281,7 +285,7 @@ fn maybe_trim_filename(name_in: String, display_data: &DisplayData) -> String { } pub fn format_string( - node: &Node, + node: &DisplayNode, indent: &str, percent_bar: &str, is_biggest: bool, @@ -294,7 +298,7 @@ pub fn format_string( } fn get_name_percent( - node: &Node, + node: &DisplayNode, indent: &str, bar_chart: &str, display_data: &DisplayData, @@ -311,7 +315,7 @@ fn get_name_percent( } } -fn get_pretty_size(node: &Node, is_biggest: bool, display_data: &DisplayData) -> String { +fn get_pretty_size(node: &DisplayNode, is_biggest: bool, display_data: &DisplayData) -> String { let output = if display_data.by_filecount { let size_as_str = node.size.separate_with_commas(); let spaces_to_add = @@ -328,7 +332,11 @@ fn get_pretty_size(node: &Node, is_biggest: bool, display_data: &DisplayData) -> } } -fn get_pretty_name(node: &Node, name_and_padding: String, display_data: &DisplayData) -> String { +fn get_pretty_name( + node: &DisplayNode, + name_and_padding: String, + display_data: &DisplayData, +) -> String { if display_data.colors_on { let meta_result = fs::metadata(node.name.clone()); let directory_color = display_data @@ -379,7 +387,7 @@ mod tests { #[test] fn test_format_str() { - let n = Node { + let n = DisplayNode { name: PathBuf::from("/short"), size: 2_u64.pow(12), // This is 4.0K children: vec![], @@ -401,7 +409,7 @@ mod tests { #[test] fn test_format_str_long_name() { let name = "very_long_name_longer_than_the_eighty_character_limit_very_long_name_this_bit_will_truncate"; - let n = Node { + let n = DisplayNode { name: PathBuf::from(name), size: 2_u64.pow(12), // This is 4.0K children: vec![], diff --git a/src/display_node.rs b/src/display_node.rs new file mode 100644 index 00000000..6ae559f5 --- /dev/null +++ b/src/display_node.rs @@ -0,0 +1,46 @@ +use std::cmp::Ordering; +use std::path::PathBuf; + +#[derive(Debug, Eq, Clone)] +pub struct DisplayNode { + pub name: PathBuf, //todo: consider moving to a string? + pub size: u64, + pub children: Vec, +} + +impl Ord for DisplayNode { + fn cmp(&self, other: &Self) -> Ordering { + if self.size == other.size { + self.name.cmp(&other.name) + } else { + self.size.cmp(&other.size) + } + } +} + +impl PartialOrd for DisplayNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for DisplayNode { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.size == other.size && self.children == other.children + } +} + +impl DisplayNode { + pub fn num_siblings(&self) -> u64 { + self.children.len() as u64 + } + + pub fn get_children_from_node(&self, is_reversed: bool) -> impl Iterator { + if is_reversed { + let children: Vec = self.children.clone().into_iter().rev().collect(); + children.into_iter() + } else { + self.children.clone().into_iter() + } + } +} diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 00000000..623abe37 --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,104 @@ +use crate::display_node::DisplayNode; +use crate::node::Node; +use std::collections::BinaryHeap; +use std::collections::HashSet; +use std::path::PathBuf; + +pub fn get_by_depth(top_level_nodes: Vec, n: usize) -> Option { + if top_level_nodes.is_empty() { + // perhaps change this, bring back Error object? + return None; + } + let root = get_new_root(top_level_nodes); + Some(build_by_depth(&root, n - 1)) +} + +pub fn get_biggest(top_level_nodes: Vec, n: usize) -> Option { + if top_level_nodes.is_empty() { + // perhaps change this, bring back Error object? + return None; + } + + let mut heap = BinaryHeap::new(); + let number_top_level_nodes = top_level_nodes.len(); + let root = get_new_root(top_level_nodes); + + root.children.iter().for_each(|c| heap.push(c)); + + let mut allowed_nodes = HashSet::new(); + allowed_nodes.insert(&root.name); + + for _ in number_top_level_nodes..n { + let line = heap.pop(); + match line { + Some(line) => { + line.children.iter().for_each(|c| heap.push(c)); + allowed_nodes.insert(&line.name); + } + None => break, + } + } + recursive_rebuilder(&allowed_nodes, &root) +} + +fn build_by_depth(node: &Node, depth: usize) -> DisplayNode { + let new_children = { + if depth == 0 { + vec![] + } else { + let mut new_children: Vec<_> = node + .children + .iter() + .map(|c| build_by_depth(c, depth - 1)) + .collect(); + new_children.sort(); + new_children.reverse(); + new_children + } + }; + + DisplayNode { + name: node.name.clone(), + size: node.size, + children: new_children, + } +} + +fn get_new_root(top_level_nodes: Vec) -> Node { + if top_level_nodes.len() > 1 { + let total_size = top_level_nodes.iter().map(|node| node.size).sum(); + Node { + name: PathBuf::from("(total)"), + size: total_size, + children: top_level_nodes, + inode_device: None, + } + } else { + top_level_nodes.into_iter().next().unwrap() + } +} + +fn recursive_rebuilder<'a>( + allowed_nodes: &'a HashSet<&PathBuf>, + current: &Node, +) -> Option { + let mut new_children: Vec<_> = current + .children + .iter() + .filter_map(|c| { + if allowed_nodes.contains(&c.name) { + recursive_rebuilder(allowed_nodes, c) + } else { + None + } + }) + .collect(); + new_children.sort(); + new_children.reverse(); + let newnode = DisplayNode { + name: current.name.clone(), + size: current.size, + children: new_children, + }; + Some(newnode) +} diff --git a/src/main.rs b/src/main.rs index 1272c9eb..4774ddcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,25 @@ #[macro_use] extern crate clap; -extern crate crossbeam_channel as channel; -extern crate ignore; +extern crate rayon; extern crate unicode_width; -extern crate walkdir; + +use std::collections::HashSet; use self::display::draw_it; -use crate::utils::is_a_parent_of; use clap::{App, AppSettings, Arg}; +use dirwalker::walk_it; +use filter::{get_biggest, get_by_depth}; use std::cmp::max; use std::path::PathBuf; use terminal_size::{terminal_size, Height, Width}; -use utils::{find_big_ones, get_dir_tree, simplify_dir_names, sort, Node}; +use utils::simplify_dir_names; +mod dirwalker; mod display; +mod display_node; +mod filter; +mod node; +mod platform; mod utils; static DEFAULT_NUMBER_OF_LINES: usize = 30; @@ -101,12 +107,6 @@ fn main() { .multiple(true) .help("Exclude any file or directory with this name"), ) - .arg( - Arg::with_name("limit_filesystem") - .short("x") - .long("limit-filesystem") - .help("Only count the files and directories on the same filesystem as the supplied directory"), - ) .arg( Arg::with_name("display_apparent_size") .short("s") @@ -184,31 +184,35 @@ fn main() { let no_colors = init_color(options.is_present("no_colors")); let use_apparent_size = options.is_present("display_apparent_size"); - let limit_filesystem = options.is_present("limit_filesystem"); - let ignore_directories = options + let ignore_directories: Vec = options .values_of("ignore_directory") - .map(|i| i.map(PathBuf::from).collect()); + .map(|i| i.map(PathBuf::from).collect()) + .unwrap_or_default(); let by_filecount = options.is_present("by_filecount"); - let show_hidden = !options.is_present("ignore_hidden"); + let ignore_hidden = options.is_present("ignore_hidden"); let simplified_dirs = simplify_dir_names(target_dirs); - let (errors, nodes) = get_dir_tree( - &simplified_dirs, - &ignore_directories, + + let ignored_full_path: HashSet = ignore_directories + .into_iter() + .flat_map(|x| simplified_dirs.iter().map(move |d| d.join(x.clone()))) + .collect(); + + let (nodes, errors) = walk_it( + simplified_dirs, + ignored_full_path, use_apparent_size, - limit_filesystem, by_filecount, - show_hidden, + ignore_hidden, ); - let sorted_data = sort(nodes); - let biggest_ones = { + + let tree = { match depth { - None => find_big_ones(sorted_data, number_of_lines), - Some(_) => sorted_data, + None => get_biggest(nodes, number_of_lines), + Some(depth) => get_by_depth(nodes, depth), } }; - let tree = build_tree(biggest_ones, depth); draw_it( errors, @@ -221,35 +225,3 @@ fn main() { tree, ); } - -fn build_tree(biggest_ones: Vec<(PathBuf, u64)>, depth: Option) -> Node { - let mut top_parent = Node::default(); - - // assume sorted order - for b in biggest_ones { - let n = Node { - name: b.0, - size: b.1, - children: Vec::default(), - }; - recursively_build_tree(&mut top_parent, n, depth); - } - top_parent -} - -fn recursively_build_tree(parent_node: &mut Node, new_node: Node, depth: Option) { - let new_depth = match depth { - None => None, - Some(0) => return, - Some(d) => Some(d - 1), - }; - if let Some(c) = parent_node - .children - .iter_mut() - .find(|c| is_a_parent_of(&c.name, &new_node.name)) - { - recursively_build_tree(c, new_node, new_depth); - } else { - parent_node.children.push(new_node); - } -} diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 00000000..d7b17f65 --- /dev/null +++ b/src/node.rs @@ -0,0 +1,54 @@ +use crate::platform::get_metadata; + +use std::cmp::Ordering; +use std::path::PathBuf; + +#[derive(Debug, Eq, Clone)] +pub struct Node { + pub name: PathBuf, + pub size: u64, + pub children: Vec, + pub inode_device: Option<(u64, u64)>, +} + +pub fn build_node( + dir: PathBuf, + children: Vec, + use_apparent_size: bool, + by_filecount: bool, +) -> Option { + match get_metadata(&dir, use_apparent_size) { + Some(data) => { + let (size, inode_device) = if by_filecount { (1, data.1) } else { data }; + Some(Node { + name: dir, + size, + children, + inode_device, + }) + } + None => None, + } +} + +impl PartialEq for Node { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.size == other.size && self.children == other.children + } +} + +impl Ord for Node { + fn cmp(&self, other: &Self) -> Ordering { + if self.size == other.size { + self.name.cmp(&other.name) + } else { + self.size.cmp(&other.size) + } + } +} + +impl PartialOrd for Node { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/utils/platform.rs b/src/platform.rs similarity index 92% rename from src/utils/platform.rs rename to src/platform.rs index c9e91078..82bfca6d 100644 --- a/src/utils/platform.rs +++ b/src/platform.rs @@ -1,7 +1,8 @@ -use ignore::DirEntry; #[allow(unused_imports)] use std::fs; +use std::path::Path; + #[cfg(target_family = "unix")] fn get_block_size() -> u64 { // All os specific implementations of MetatdataExt seem to define a block as 512 bytes @@ -10,7 +11,7 @@ fn get_block_size() -> u64 { } #[cfg(target_family = "unix")] -pub fn get_metadata(d: &DirEntry, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> { +pub fn get_metadata(d: &Path, use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> { use std::os::unix::fs::MetadataExt; match d.metadata() { Ok(md) => { @@ -25,7 +26,7 @@ pub fn get_metadata(d: &DirEntry, use_apparent_size: bool) -> Option<(u64, Optio } #[cfg(target_family = "windows")] -pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> { +pub fn get_metadata(d: &Path, _use_apparent_size: bool) -> Option<(u64, Option<(u64, u64)>)> { // On windows opening the file to get size, file ID and volume can be very // expensive because 1) it causes a few system calls, and more importantly 2) it can cause // windows defender to scan the file. @@ -63,7 +64,6 @@ pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Opti // With this optimization: 8 sec. use std::io; - use std::path::Path; use winapi_util::Handle; fn handle_from_path_limited>(path: P) -> io::Result { use std::fs::OpenOptions; @@ -90,10 +90,10 @@ pub fn get_metadata(d: &DirEntry, _use_apparent_size: bool) -> Option<(u64, Opti Ok(Handle::from_file(file)) } - fn get_metadata_expensive(d: &DirEntry) -> Option<(u64, Option<(u64, u64)>)> { + fn get_metadata_expensive(d: &Path) -> Option<(u64, Option<(u64, u64)>)> { use winapi_util::file::information; - let h = handle_from_path_limited(d.path()).ok()?; + let h = handle_from_path_limited(d).ok()?; let info = information(&h).ok()?; Some(( diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..8d587287 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,126 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +fn is_a_parent_of>(parent: P, child: P) -> bool { + let parent = parent.as_ref(); + let child = child.as_ref(); + child.starts_with(parent) && !parent.starts_with(child) +} + +pub fn simplify_dir_names>(filenames: Vec

) -> HashSet { + let mut top_level_names: HashSet = HashSet::with_capacity(filenames.len()); + let mut to_remove: Vec = Vec::with_capacity(filenames.len()); + + for t in filenames { + let top_level_name = normalize_path(t); + let mut can_add = true; + + for tt in top_level_names.iter() { + if is_a_parent_of(&top_level_name, tt) { + to_remove.push(tt.to_path_buf()); + } else if is_a_parent_of(tt, &top_level_name) { + can_add = false; + } + } + to_remove.sort_unstable(); + top_level_names.retain(|tr| to_remove.binary_search(tr).is_err()); + to_remove.clear(); + if can_add { + top_level_names.insert(top_level_name); + } + } + + top_level_names +} + +pub fn normalize_path>(path: P) -> PathBuf { + // normalize path ... + // 1. removing repeated separators + // 2. removing interior '.' ("current directory") path segments + // 3. removing trailing extra separators and '.' ("current directory") path segments + // * `Path.components()` does all the above work; ref: + // 4. changing to os preferred separator (automatically done by recollecting components back into a PathBuf) + path.as_ref().components().collect::() +} + +mod tests { + #[allow(unused_imports)] + use super::*; + + #[test] + fn test_simplify_dir() { + let mut correct = HashSet::new(); + correct.insert(PathBuf::from("a")); + assert_eq!(simplify_dir_names(vec!["a"]), correct); + } + + #[test] + fn test_simplify_dir_rm_subdir() { + let mut correct = HashSet::new(); + correct.insert(["a", "b"].iter().collect::()); + assert_eq!(simplify_dir_names(vec!["a/b", "a/b/c", "a/b/d/f"]), correct); + } + + #[test] + fn test_simplify_dir_duplicates() { + let mut correct = HashSet::new(); + correct.insert(["a", "b"].iter().collect::()); + correct.insert(PathBuf::from("c")); + assert_eq!( + simplify_dir_names(vec![ + "a/b", + "a/b//", + "a/././b///", + "c", + "c/", + "c/.", + "c/././", + "c/././." + ]), + correct + ); + } + #[test] + fn test_simplify_dir_rm_subdir_and_not_substrings() { + let mut correct = HashSet::new(); + correct.insert(PathBuf::from("b")); + correct.insert(["c", "a", "b"].iter().collect::()); + correct.insert(["a", "b"].iter().collect::()); + assert_eq!(simplify_dir_names(vec!["a/b", "c/a/b/", "b"]), correct); + } + + #[test] + fn test_simplify_dir_dots() { + let mut correct = HashSet::new(); + correct.insert(PathBuf::from("src")); + assert_eq!(simplify_dir_names(vec!["src/."]), correct); + } + + #[test] + fn test_simplify_dir_substring_names() { + let mut correct = HashSet::new(); + correct.insert(PathBuf::from("src")); + correct.insert(PathBuf::from("src_v2")); + assert_eq!(simplify_dir_names(vec!["src/", "src_v2"]), correct); + } + + #[test] + fn test_is_a_parent_of() { + assert!(is_a_parent_of("/usr", "/usr/andy")); + assert!(is_a_parent_of("/usr", "/usr/andy/i/am/descendant")); + assert!(!is_a_parent_of("/usr", "/usr/.")); + assert!(!is_a_parent_of("/usr", "/usr/")); + assert!(!is_a_parent_of("/usr", "/usr")); + assert!(!is_a_parent_of("/usr/", "/usr")); + assert!(!is_a_parent_of("/usr/andy", "/usr")); + assert!(!is_a_parent_of("/usr/andy", "/usr/sibling")); + assert!(!is_a_parent_of("/usr/folder", "/usr/folder_not_a_child")); + } + + #[test] + fn test_is_a_parent_of_root() { + assert!(is_a_parent_of("/", "/usr/andy")); + assert!(is_a_parent_of("/", "/usr")); + assert!(!is_a_parent_of("/", "/")); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index f2e07db6..00000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,402 +0,0 @@ -use std::cmp::Ordering; -use std::collections::HashMap; -use std::collections::HashSet; -use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; - -use channel::Receiver; -use std::thread::JoinHandle; - -use ignore::{WalkBuilder, WalkState}; -use std::sync::atomic; -use std::thread; - -mod platform; -use self::platform::*; - -type PathData = (PathBuf, u64, Option<(u64, u64)>); - -#[derive(Debug, Default, Eq, Clone)] -pub struct Node { - pub name: PathBuf, - pub size: u64, - pub children: Vec, -} - -impl Ord for Node { - fn cmp(&self, other: &Self) -> Ordering { - if self.size == other.size { - self.name.cmp(&other.name) - } else { - self.size.cmp(&other.size) - } - } -} - -impl PartialOrd for Node { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for Node { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.size == other.size && self.children == other.children - } -} - -impl Node { - pub fn num_siblings(&self) -> u64 { - self.children.len() as u64 - } - - pub fn get_children_from_node(&self, is_reversed: bool) -> impl Iterator { - if is_reversed { - let children: Vec = self.children.clone().into_iter().rev().collect(); - children.into_iter() - } else { - self.children.clone().into_iter() - } - } -} - -pub struct Errors { - pub permissions: bool, - pub not_found: bool, -} - -pub fn is_a_parent_of>(parent: P, child: P) -> bool { - let parent = parent.as_ref(); - let child = child.as_ref(); - child.starts_with(parent) && !parent.starts_with(child) -} - -pub fn simplify_dir_names>(filenames: Vec

) -> HashSet { - let mut top_level_names: HashSet = HashSet::with_capacity(filenames.len()); - let mut to_remove: Vec = Vec::with_capacity(filenames.len()); - - for t in filenames { - let top_level_name = normalize_path(t); - let mut can_add = true; - - for tt in top_level_names.iter() { - if is_a_parent_of(&top_level_name, tt) { - to_remove.push(tt.to_path_buf()); - } else if is_a_parent_of(tt, &top_level_name) { - can_add = false; - } - } - to_remove.sort_unstable(); - top_level_names.retain(|tr| to_remove.binary_search(tr).is_err()); - to_remove.clear(); - if can_add { - top_level_names.insert(top_level_name); - } - } - - top_level_names -} - -fn prepare_walk_dir_builder>( - top_level_names: &HashSet

, - limit_filesystem: bool, - show_hidden: bool, -) -> WalkBuilder { - let mut it = top_level_names.iter(); - let mut builder = WalkBuilder::new(it.next().unwrap()); - builder.follow_links(false); - if show_hidden { - builder.hidden(false); - builder.ignore(false); - builder.git_global(false); - builder.git_ignore(false); - builder.git_exclude(false); - } - - if limit_filesystem { - builder.same_file_system(true); - } - - for b in it { - builder.add(b); - } - builder -} - -fn is_not_found(e: &ignore::Error) -> bool { - use ignore::Error; - if let Error::WithPath { err, .. } = e { - if let Error::Io(e) = &**err { - if e.kind() == std::io::ErrorKind::NotFound { - return true; - } - } - } - false -} - -pub fn get_dir_tree>( - top_level_names: &HashSet

, - ignore_directories: &Option>, - apparent_size: bool, - limit_filesystem: bool, - by_filecount: bool, - show_hidden: bool, -) -> (Errors, HashMap) { - let (tx, rx) = channel::bounded::(1000); - - let permissions_flag = AtomicBool::new(false); - let not_found_flag = AtomicBool::new(false); - - let t2 = top_level_names - .iter() - .map(|p| p.as_ref().to_path_buf()) - .collect(); - - let t = create_reader_thread(rx, t2, apparent_size); - let walk_dir_builder = prepare_walk_dir_builder(top_level_names, limit_filesystem, show_hidden); - - walk_dir_builder.build_parallel().run(|| { - let txc = tx.clone(); - let pf = &permissions_flag; - let nf = ¬_found_flag; - Box::new(move |path| { - match path { - Ok(p) => { - if let Some(dirs) = ignore_directories { - let path = p.path(); - let parts = path.components().collect::>(); - for d in dirs { - if parts - .windows(d.components().count()) - .any(|window| window.iter().collect::() == *d) - { - return WalkState::Continue; - } - } - } - - let maybe_size_and_inode = get_metadata(&p, apparent_size); - - match maybe_size_and_inode { - Some(data) => { - let (size, inode_device) = - if by_filecount { (1, data.1) } else { data }; - txc.send((p.into_path(), size, inode_device)).unwrap(); - } - None => { - pf.store(true, atomic::Ordering::Relaxed); - } - } - } - Err(e) => { - if is_not_found(&e) { - nf.store(true, atomic::Ordering::Relaxed); - } else { - pf.store(true, atomic::Ordering::Relaxed); - } - } - }; - WalkState::Continue - }) - }); - - drop(tx); - let data = t.join().unwrap(); - let errors = Errors { - permissions: permissions_flag.load(atomic::Ordering::SeqCst), - not_found: not_found_flag.load(atomic::Ordering::SeqCst), - }; - (errors, data) -} - -fn create_reader_thread( - rx: Receiver, - top_level_names: HashSet, - apparent_size: bool, -) -> JoinHandle> { - // Receiver thread - thread::spawn(move || { - let mut hash: HashMap = HashMap::new(); - let mut inodes: HashSet<(u64, u64)> = HashSet::new(); - - for dent in rx { - let (path, size, maybe_inode_device) = dent; - - if should_ignore_file(apparent_size, &mut inodes, maybe_inode_device) { - continue; - } else { - for p in path.ancestors() { - let s = hash.entry(p.to_path_buf()).or_insert(0); - *s += size; - - if top_level_names.contains(p) { - break; - } - } - } - } - hash - }) -} - -pub fn normalize_path>(path: P) -> PathBuf { - // normalize path ... - // 1. removing repeated separators - // 2. removing interior '.' ("current directory") path segments - // 3. removing trailing extra separators and '.' ("current directory") path segments - // * `Path.components()` does all the above work; ref: - // 4. changing to os preferred separator (automatically done by recollecting components back into a PathBuf) - path.as_ref().components().collect::() -} - -fn should_ignore_file( - apparent_size: bool, - inodes: &mut HashSet<(u64, u64)>, - maybe_inode_device: Option<(u64, u64)>, -) -> bool { - match maybe_inode_device { - None => false, - Some(data) => { - let (inode, device) = data; - if !apparent_size { - // Ignore files already visited or symlinked - if inodes.contains(&(inode, device)) { - return true; - } - inodes.insert((inode, device)); - } - false - } - } -} - -pub fn sort_by_size_first_name_second(a: &(PathBuf, u64), b: &(PathBuf, u64)) -> Ordering { - let result = b.1.cmp(&a.1); - if result == Ordering::Equal { - a.0.cmp(&b.0) - } else { - result - } -} - -pub fn sort(data: HashMap) -> Vec<(PathBuf, u64)> { - let mut new_l: Vec<(PathBuf, u64)> = data.iter().map(|(a, b)| (a.clone(), *b)).collect(); - new_l.sort_unstable_by(sort_by_size_first_name_second); - new_l -} - -pub fn find_big_ones(new_l: Vec<(PathBuf, u64)>, max_to_show: usize) -> Vec<(PathBuf, u64)> { - if max_to_show > 0 && new_l.len() > max_to_show { - new_l[0..max_to_show].to_vec() - } else { - new_l - } -} - -mod tests { - #[allow(unused_imports)] - use super::*; - - #[test] - fn test_simplify_dir() { - let mut correct = HashSet::new(); - correct.insert(PathBuf::from("a")); - assert_eq!(simplify_dir_names(vec!["a"]), correct); - } - - #[test] - fn test_simplify_dir_rm_subdir() { - let mut correct = HashSet::new(); - correct.insert(["a", "b"].iter().collect::()); - assert_eq!(simplify_dir_names(vec!["a/b", "a/b/c", "a/b/d/f"]), correct); - } - - #[test] - fn test_simplify_dir_duplicates() { - let mut correct = HashSet::new(); - correct.insert(["a", "b"].iter().collect::()); - correct.insert(PathBuf::from("c")); - assert_eq!( - simplify_dir_names(vec![ - "a/b", - "a/b//", - "a/././b///", - "c", - "c/", - "c/.", - "c/././", - "c/././." - ]), - correct - ); - } - #[test] - fn test_simplify_dir_rm_subdir_and_not_substrings() { - let mut correct = HashSet::new(); - correct.insert(PathBuf::from("b")); - correct.insert(["c", "a", "b"].iter().collect::()); - correct.insert(["a", "b"].iter().collect::()); - assert_eq!(simplify_dir_names(vec!["a/b", "c/a/b/", "b"]), correct); - } - - #[test] - fn test_simplify_dir_dots() { - let mut correct = HashSet::new(); - correct.insert(PathBuf::from("src")); - assert_eq!(simplify_dir_names(vec!["src/."]), correct); - } - - #[test] - fn test_simplify_dir_substring_names() { - let mut correct = HashSet::new(); - correct.insert(PathBuf::from("src")); - correct.insert(PathBuf::from("src_v2")); - assert_eq!(simplify_dir_names(vec!["src/", "src_v2"]), correct); - } - - #[test] - fn test_is_a_parent_of() { - assert!(is_a_parent_of("/usr", "/usr/andy")); - assert!(is_a_parent_of("/usr", "/usr/andy/i/am/descendant")); - assert!(!is_a_parent_of("/usr", "/usr/.")); - assert!(!is_a_parent_of("/usr", "/usr/")); - assert!(!is_a_parent_of("/usr", "/usr")); - assert!(!is_a_parent_of("/usr/", "/usr")); - assert!(!is_a_parent_of("/usr/andy", "/usr")); - assert!(!is_a_parent_of("/usr/andy", "/usr/sibling")); - assert!(!is_a_parent_of("/usr/folder", "/usr/folder_not_a_child")); - } - - #[test] - fn test_is_a_parent_of_root() { - assert!(is_a_parent_of("/", "/usr/andy")); - assert!(is_a_parent_of("/", "/usr")); - assert!(!is_a_parent_of("/", "/")); - } - - #[test] - fn test_should_ignore_file() { - let mut files = HashSet::new(); - files.insert((10, 20)); - - assert!(!should_ignore_file(true, &mut files, Some((0, 0)))); - - // New file is not known it will be inserted to the hashmp and should not be ignored - assert!(!should_ignore_file(false, &mut files, Some((11, 12)))); - assert!(files.contains(&(11, 12))); - - // The same file will be ignored the second time - assert!(should_ignore_file(false, &mut files, Some((11, 12)))); - } - - #[test] - fn test_should_ignore_file_on_different_device() { - let mut files = HashSet::new(); - files.insert((10, 20)); - - // We do not ignore files on the same device - assert!(!should_ignore_file(false, &mut files, Some((2, 99)))); - assert!(!should_ignore_file(true, &mut files, Some((2, 99)))); - } -} diff --git a/tests/test_exact_output.rs b/tests/test_exact_output.rs index fbce5ca4..8b3178b3 100644 --- a/tests/test_exact_output.rs +++ b/tests/test_exact_output.rs @@ -114,8 +114,6 @@ pub fn test_main_long_paths() { .unwrap() .stdout; let output = str::from_utf8(&assert).unwrap(); - println!("{:?}", output.trim()); - println!("{:?}", main_output_long_paths().trim()); assert!(output.contains(&main_output_long_paths())); } @@ -204,9 +202,9 @@ fn no_substring_of_names_output() -> String { 0B ┌── long_dir_name_what_a_very_long_dir_name_what_happens_when_this_g.. 4.0K ├── dir_name_clash 4.0K │ ┌── hello - 8.0K ├─┴ dir_substring - 4.0K │ ┌── hello 8.0K ├─┴ dir + 4.0K │ ┌── hello + 8.0K ├─┴ dir_substring 24K ┌─┴ test_dir2 " .trim() @@ -218,10 +216,10 @@ fn no_substring_of_names_output() -> String { " 0B ┌── long_dir_name_what_a_very_long_dir_name_what_happens_when_this_g.. 4.0K │ ┌── hello - 4.0K ├─┴ dir_substring + 4.0K ├─┴ dir 4.0K ├── dir_name_clash 4.0K │ ┌── hello - 4.0K ├─┴ dir + 4.0K ├─┴ dir_substring 12K ┌─┴ test_dir2 " .trim() @@ -247,8 +245,8 @@ pub fn test_unicode_directories() { fn unicode_dir() -> String { // The way unicode & asian characters are rendered on the terminal should make this line up " - 0B ┌── 👩.unicode │ █ │ 0% - 0B ├── ラウトは難しいです!.japan│ █ │ 0% + 0B ┌── ラウトは難しいです!.japan│ █ │ 0% + 0B ├── 👩.unicode │ █ │ 0% 4.0K ┌─┴ test_dir_unicode │██████████████████████████████████ │ 100% " .trim() @@ -258,8 +256,8 @@ fn unicode_dir() -> String { #[cfg(target_os = "macos")] fn unicode_dir() -> String { " - 0B ┌── 👩.unicode │ █ │ 0% - 0B ├── ラウトは難しいです!.japan│ █ │ 0% + 0B ┌── ラウトは難しいです!.japan│ █ │ 0% + 0B ├── 👩.unicode │ █ │ 0% 0B ┌─┴ test_dir_unicode │ █ │ 0% " .trim() diff --git a/tests/test_flags.rs b/tests/test_flags.rs index 6dbaf4a4..f4a938b6 100644 --- a/tests/test_flags.rs +++ b/tests/test_flags.rs @@ -142,8 +142,8 @@ pub fn test_number_of_files() { .unwrap() .stdout; let output = str::from_utf8(&output).unwrap(); - assert!(output.contains("1 ┌── hello_file")); - assert!(output.contains("1 ├── a_file ")); + assert!(output.contains("1 ┌── a_file ")); + assert!(output.contains("1 ├── hello_file")); assert!(output.contains("3 ┌─┴ many")); assert!(output.contains("4 ┌─┴ test_dir")); } diff --git a/tests/tests_symlinks.rs b/tests/tests_symlinks.rs index 2dfd4c33..6b1f08fb 100644 --- a/tests/tests_symlinks.rs +++ b/tests/tests_symlinks.rs @@ -2,7 +2,6 @@ use assert_cmd::Command; use std::cmp::max; use std::fs::File; use std::io::Write; -use std::panic; use std::path::PathBuf; use std::str; @@ -61,12 +60,12 @@ pub fn test_soft_sym_link() { .output(); assert!(c.is_ok()); - let c = format!(" ┌── {}", get_file_name(link_name_s.into())); - let b = format!(" ├── {}", get_file_name(file_path_s.into())); + let c = format!(" ├── {}", get_file_name(link_name_s.into())); + let b = format!(" ┌── {}", get_file_name(file_path_s.into())); let a = format!("─┴ {}", dir_s); let mut cmd = Command::cargo_bin("dust").unwrap(); - let output = cmd.arg("-p").arg("-c").arg(dir_s).unwrap().stdout; + let output = cmd.arg("-p").arg("-c").arg("-s").arg(dir_s).unwrap().stdout; let output = str::from_utf8(&output).unwrap(); @@ -125,9 +124,16 @@ pub fn test_recursive_sym_link() { let b = format!(" └── {}", get_file_name(link_name_s.into())); let mut cmd = Command::cargo_bin("dust").unwrap(); - let output = cmd.arg("-p").arg("-c").arg("-r").arg(dir_s).unwrap().stdout; - + let output = cmd + .arg("-p") + .arg("-c") + .arg("-r") + .arg("-s") + .arg(dir_s) + .unwrap() + .stdout; let output = str::from_utf8(&output).unwrap(); + assert!(output.contains(a.as_str())); assert!(output.contains(b.as_str())); }