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