1 use anyhow::{Context, Error, Result};
2 use dua::{
3     traverse::{EntryData, Tree, TreeIndex},
4     ByteFormat, TraversalSorting, WalkOptions,
5 };
6 use itertools::Itertools;
7 use jwalk::{DirEntry, WalkDir};
8 use petgraph::prelude::NodeIndex;
9 use std::{
10     env::temp_dir,
11     ffi::OsStr,
12     fmt,
13     fs::{copy, create_dir_all, remove_dir, remove_file},
14     io::ErrorKind,
15     path::{Path, PathBuf},
16 };
17 use tui::backend::TestBackend;
18 use tui_react::Terminal;
19 
20 use crate::interactive::{app::tests::FIXTURE_PATH, Interaction, TerminalApp};
21 
into_keys<'a>( bytes: impl Iterator<Item = &'a u8> + 'a, ) -> impl Iterator<Item = crosstermion::input::Key> + 'a22 pub fn into_keys<'a>(
23     bytes: impl Iterator<Item = &'a u8> + 'a,
24 ) -> impl Iterator<Item = crosstermion::input::Key> + 'a {
25     bytes.map(|b| crosstermion::input::Key::Char(std::char::from_u32(*b as u32).unwrap()))
26 }
27 
node_by_index(app: &TerminalApp, id: TreeIndex) -> &EntryData28 pub fn node_by_index(app: &TerminalApp, id: TreeIndex) -> &EntryData {
29     app.traversal.tree.node_weight(id).unwrap()
30 }
31 
node_by_name(app: &TerminalApp, name: impl AsRef<OsStr>) -> &EntryData32 pub fn node_by_name(app: &TerminalApp, name: impl AsRef<OsStr>) -> &EntryData {
33     node_by_index(app, index_by_name(app, name))
34 }
35 
index_by_name_and_size( app: &TerminalApp, name: impl AsRef<OsStr>, size: Option<u128>, ) -> TreeIndex36 pub fn index_by_name_and_size(
37     app: &TerminalApp,
38     name: impl AsRef<OsStr>,
39     size: Option<u128>,
40 ) -> TreeIndex {
41     let name = name.as_ref();
42     let t: Vec<_> = app
43         .traversal
44         .tree
45         .node_indices()
46         .map(|idx| (idx, node_by_index(app, idx)))
47         .filter_map(|(idx, e)| {
48             if e.name == name && size.map(|s| s == e.size).unwrap_or(true) {
49                 Some(idx)
50             } else {
51                 None
52             }
53         })
54         .collect();
55     match t.len() {
56         1 => t[0],
57         0 => panic!("Node named '{}' not found in tree", name.to_string_lossy()),
58         n => panic!("Node named '{}' found {} times", name.to_string_lossy(), n),
59     }
60 }
61 
index_by_name(app: &TerminalApp, name: impl AsRef<OsStr>) -> TreeIndex62 pub fn index_by_name(app: &TerminalApp, name: impl AsRef<OsStr>) -> TreeIndex {
63     index_by_name_and_size(app, name, None)
64 }
65 
66 pub struct WritableFixture {
67     pub root: PathBuf,
68 }
69 
70 impl Drop for WritableFixture {
drop(&mut self)71     fn drop(&mut self) {
72         delete_recursive(&self.root).ok();
73     }
74 }
75 
delete_recursive(path: impl AsRef<Path>) -> Result<()>76 fn delete_recursive(path: impl AsRef<Path>) -> Result<()> {
77     let mut files: Vec<_> = Vec::new();
78     let mut dirs: Vec<_> = Vec::new();
79 
80     for entry in WalkDir::new(&path)
81         .parallelism(jwalk::Parallelism::Serial)
82         .into_iter()
83     {
84         let entry: DirEntry<_> = entry?;
85         let p = entry.path();
86         match p.is_dir() {
87             true => dirs.push(p),
88             false => files.push(p),
89         }
90     }
91 
92     files
93         .iter()
94         .map(|f| remove_file(f).map_err(Error::from))
95         .chain(
96             dirs.iter()
97                 .sorted_by_key(|p| p.components().count())
98                 .rev()
99                 .map(|d| {
100                     remove_dir(d)
101                         .with_context(|| format!("Could not delete '{}'", d.display()))
102                         .map_err(Error::from)
103                 }),
104         )
105         .collect::<Result<_, _>>()
106 }
107 
copy_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error>108 fn copy_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {
109     for entry in WalkDir::new(&src)
110         .parallelism(jwalk::Parallelism::Serial)
111         .into_iter()
112     {
113         let entry: DirEntry<_> = entry?;
114         let entry_path = entry.path();
115         entry_path
116             .strip_prefix(&src)
117             .map_err(Error::from)
118             .and_then(|relative_entry_path| {
119                 let dst = dst.as_ref().join(relative_entry_path);
120                 if entry_path.is_dir() {
121                     create_dir_all(dst).map_err(Into::into)
122                 } else {
123                     copy(&entry_path, dst)
124                         .map(|_| ())
125                         .or_else(|e| match e.kind() {
126                             ErrorKind::AlreadyExists => Ok(()),
127                             _ => Err(e),
128                         })
129                         .map_err(Into::into)
130                 }
131             })?;
132     }
133     Ok(())
134 }
135 
136 impl From<&'static str> for WritableFixture {
from(fixture_name: &str) -> Self137     fn from(fixture_name: &str) -> Self {
138         const TEMP_TLD_DIRNAME: &str = "dua-unit";
139 
140         let src = fixture(fixture_name);
141         let dst = temp_dir().join(TEMP_TLD_DIRNAME);
142         create_dir_all(&dst).unwrap();
143 
144         let dst = dst.join(fixture_name);
145         copy_recursive(src, &dst).unwrap();
146         WritableFixture { root: dst }
147     }
148 }
149 
150 impl AsRef<Path> for WritableFixture {
as_ref(&self) -> &Path151     fn as_ref(&self) -> &Path {
152         &self.root
153     }
154 }
155 
fixture(p: impl AsRef<Path>) -> PathBuf156 pub fn fixture(p: impl AsRef<Path>) -> PathBuf {
157     Path::new(FIXTURE_PATH).join(p)
158 }
159 
fixture_str(p: impl AsRef<Path>) -> String160 pub fn fixture_str(p: impl AsRef<Path>) -> String {
161     fixture(p).to_str().unwrap().to_owned()
162 }
163 
initialized_app_and_terminal_with_closure( fixture_paths: &[impl AsRef<Path>], mut convert: impl FnMut(&Path) -> PathBuf, ) -> Result<(Terminal<TestBackend>, TerminalApp), Error>164 pub fn initialized_app_and_terminal_with_closure(
165     fixture_paths: &[impl AsRef<Path>],
166     mut convert: impl FnMut(&Path) -> PathBuf,
167 ) -> Result<(Terminal<TestBackend>, TerminalApp), Error> {
168     let mut terminal = new_test_terminal()?;
169     std::env::set_current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))?;
170 
171     let input_paths = fixture_paths.iter().map(|c| convert(c.as_ref())).collect();
172     let app = TerminalApp::initialize(
173         &mut terminal,
174         WalkOptions {
175             threads: 1,
176             byte_format: ByteFormat::Metric,
177             apparent_size: true,
178             count_hard_links: false,
179             sorting: TraversalSorting::AlphabeticalByFileName,
180             cross_filesystems: false,
181         },
182         input_paths,
183         Interaction::None,
184     )?
185     .map(|(_, app)| app);
186     Ok((
187         terminal,
188         app.expect("app that didn't try to abort iteration"),
189     ))
190 }
191 
new_test_terminal() -> std::io::Result<Terminal<TestBackend>>192 pub fn new_test_terminal() -> std::io::Result<Terminal<TestBackend>> {
193     Terminal::new(TestBackend::new(40, 20))
194 }
195 
initialized_app_and_terminal_from_paths( fixture_paths: &[PathBuf], ) -> Result<(Terminal<TestBackend>, TerminalApp), Error>196 pub fn initialized_app_and_terminal_from_paths(
197     fixture_paths: &[PathBuf],
198 ) -> Result<(Terminal<TestBackend>, TerminalApp), Error> {
199     fn to_path_buf(p: &Path) -> PathBuf {
200         p.to_path_buf()
201     }
202     initialized_app_and_terminal_with_closure(fixture_paths, to_path_buf)
203 }
204 
initialized_app_and_terminal_from_fixture( fixture_paths: &[&str], ) -> Result<(Terminal<TestBackend>, TerminalApp), Error>205 pub fn initialized_app_and_terminal_from_fixture(
206     fixture_paths: &[&str],
207 ) -> Result<(Terminal<TestBackend>, TerminalApp), Error> {
208     #[allow(clippy::redundant_closure)]
209     // doesn't actually work that way due to borrowchk - probably a bug
210     initialized_app_and_terminal_with_closure(fixture_paths, |p| fixture(p))
211 }
212 
sample_01_tree() -> Tree213 pub fn sample_01_tree() -> Tree {
214     let mut tree = Tree::new();
215     {
216         let mut add_node = make_add_node(&mut tree);
217         #[cfg(not(windows))]
218         let root_size = 1259070;
219         #[cfg(windows)]
220         let root_size = 1259069;
221         let rn = add_node("", root_size, None);
222         {
223             let sn = add_node(&fixture_str("sample-01"), root_size, Some(rn));
224             {
225                 add_node(".hidden.666", 666, Some(sn));
226                 add_node("a", 256, Some(sn));
227                 add_node("b.empty", 0, Some(sn));
228                 #[cfg(not(windows))]
229                 add_node("c.lnk", 1, Some(sn));
230                 #[cfg(windows)]
231                 add_node("c.lnk", 0, Some(sn));
232                 let dn = add_node("dir", 1258024, Some(sn));
233                 {
234                     add_node("1000bytes", 1000, Some(dn));
235                     add_node("dir-a.1mb", 1_000_000, Some(dn));
236                     add_node("dir-a.kb", 1024, Some(dn));
237                     let en = add_node("empty-dir", 0, Some(dn));
238                     {
239                         add_node(".gitkeep", 0, Some(en));
240                     }
241                     let sub = add_node("sub", 256_000, Some(dn));
242                     {
243                         add_node("dir-sub-a.256kb", 256_000, Some(sub));
244                     }
245                 }
246                 add_node("z123.b", 123, Some(sn));
247             }
248         }
249     }
250     tree
251 }
252 
sample_02_tree() -> Tree253 pub fn sample_02_tree() -> Tree {
254     let mut tree = Tree::new();
255     {
256         let mut add_node = make_add_node(&mut tree);
257         let root_size = 1540;
258         let rn = add_node("", root_size, None);
259         {
260             let sn = add_node(
261                 Path::new(FIXTURE_PATH).join("sample-02").to_str().unwrap(),
262                 root_size,
263                 Some(rn),
264             );
265             {
266                 add_node("a", 256, Some(sn));
267                 add_node("b", 1, Some(sn));
268                 let dn = add_node("dir", 1283, Some(sn));
269                 {
270                     add_node("c", 257, Some(dn));
271                     add_node("d", 2, Some(dn));
272                     let en = add_node("empty-dir", 0, Some(dn));
273                     {
274                         add_node(".gitkeep", 0, Some(en));
275                     }
276                     let sub = add_node("sub", 1024, Some(dn));
277                     {
278                         add_node("e", 1024, Some(sub));
279                     }
280                 }
281             }
282         }
283     }
284     tree
285 }
286 
make_add_node(t: &mut Tree) -> impl FnMut(&str, u128, Option<NodeIndex>) -> NodeIndex + '_287 pub fn make_add_node(t: &mut Tree) -> impl FnMut(&str, u128, Option<NodeIndex>) -> NodeIndex + '_ {
288     move |name, size, maybe_from_idx| {
289         let n = t.add_node(EntryData {
290             name: PathBuf::from(name),
291             size,
292             metadata_io_error: false,
293         });
294         if let Some(from) = maybe_from_idx {
295             t.add_edge(from, n, ());
296         }
297         n
298     }
299 }
300 
debug(item: impl fmt::Debug) -> String301 pub fn debug(item: impl fmt::Debug) -> String {
302     format!("{:?}", item)
303 }
304