1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::Instant;
4
5use rayon::prelude::*;
6use crate::entry::{Entry, EntryKind};
9use crate::error::{Error, Result};
10use crate::filter::filter_entries;
11use crate::ignore::{apply_ignore_set, load_ignore_set, IgnoreSet};
12use crate::options::{CliSymlinkMode, ListOptions};
13use crate::sort::sort_entries;
14
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct ListTiming {
18 pub readdir_ms: u128,
20 pub stat_ms: u128,
22 pub sort_ms: u128,
24}
25
26impl ListTiming {
27 pub fn add_assign(&mut self, other: &ListTiming) {
29 self.readdir_ms = self.readdir_ms.saturating_add(other.readdir_ms);
30 self.stat_ms = self.stat_ms.saturating_add(other.stat_ms);
31 self.sort_ms = self.sort_ms.saturating_add(other.sort_ms);
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct Listing {
38 pub root: PathBuf,
40 pub root_is_dir: bool,
42 pub entries: Vec<Entry>,
44 pub minor_errors: usize,
47 pub timing: Option<ListTiming>,
49}
50
51impl Listing {
52 fn new(root: PathBuf, root_is_dir: bool, entries: Vec<Entry>) -> Self {
53 Self {
54 root,
55 root_is_dir,
56 entries,
57 minor_errors: 0,
58 timing: None,
59 }
60 }
61
62 fn with_timing(mut self, timing: Option<ListTiming>) -> Self {
63 self.timing = timing;
64 self
65 }
66}
67
68fn follow_cli_path(path: &Path, opts: &ListOptions) -> bool {
70 if opts.follow_links {
71 return true;
72 }
73 match opts.cli_symlink {
74 CliSymlinkMode::Never => false,
75 CliSymlinkMode::Always => {
76 fs::symlink_metadata(path)
78 .map(|m| m.file_type().is_symlink())
79 .unwrap_or(false)
80 }
81 CliSymlinkMode::DirOnly => {
82 let is_link = fs::symlink_metadata(path)
84 .map(|m| m.file_type().is_symlink())
85 .unwrap_or(false);
86 if !is_link {
87 return false;
88 }
89 fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
90 }
91 }
92}
93
94pub fn list_path(path: &Path, opts: &ListOptions) -> Result<Listing> {
98 let path = if path.as_os_str().is_empty() {
99 Path::new(".")
100 } else {
101 path
102 };
103
104 let follow = follow_cli_path(path, opts);
105
106 let meta = if follow {
107 fs::metadata(path)
108 } else {
109 fs::symlink_metadata(path)
110 }
111 .map_err(|source| {
112 if source.kind() == std::io::ErrorKind::NotFound {
113 Error::NotFound(path.to_path_buf())
114 } else {
115 Error::Metadata {
116 path: path.to_path_buf(),
117 source,
118 }
119 }
120 })?;
121
122 if opts.directory || !meta.is_dir() {
124 let fill = meta_fill_from(opts);
125 let entry = if follow {
126 Entry::from_path_follow_with(path, 0, fill)?
127 } else {
128 Entry::from_path_and_meta_with(path, &meta, 0, fill)?
129 };
130 return Ok(Listing::new(path.to_path_buf(), meta.is_dir(), vec![entry]));
131 }
132
133 if opts.recursive {
134 list_recursive(path, opts)
135 } else {
136 list_directory(path, opts)
137 }
138}
139
140fn meta_fill_from(opts: &ListOptions) -> crate::entry::MetaFill {
141 crate::entry::MetaFill {
142 resolve_names: opts.resolve_owner_group,
143 read_context: opts.read_selinux,
144 }
145}
146
147fn entry_from_dir_entry(
149 item: &fs::DirEntry,
150 follow_links: bool,
151 fill: crate::entry::MetaFill,
152 prefer_statx: bool,
153) -> Option<Entry> {
154 let entry_path = item.path();
155 if follow_links {
156 return Entry::from_path_follow_with(&entry_path, 0, fill).ok();
157 }
158 #[cfg(target_os = "linux")]
159 {
160 if prefer_statx {
161 if let Ok(e) = crate::linux_statx::entry_from_statx(&entry_path, 0, fill) {
162 return Some(e);
163 }
164 }
165 }
166 #[cfg(not(target_os = "linux"))]
167 {
168 let _ = prefer_statx;
169 }
170 let meta = item
171 .metadata()
172 .or_else(|_| fs::symlink_metadata(&entry_path));
173 match meta {
174 Ok(m) => Entry::from_path_and_meta_with(&entry_path, &m, 0, fill).ok(),
175 Err(_) => None,
176 }
177}
178
179fn stat_dir_entries(dir_entries: &[fs::DirEntry], opts: &ListOptions) -> Vec<Entry> {
181 let follow = opts.follow_links;
182 let fill = meta_fill_from(opts);
183 let prefer_statx = opts.linux_statx;
184 let map_one = |item: &fs::DirEntry| entry_from_dir_entry(item, follow, fill, prefer_statx);
185
186 #[cfg(all(target_os = "linux", feature = "io-uring"))]
188 {
189 if opts.io_uring
190 && !follow
191 && dir_entries.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
192 && !fill.resolve_names
193 && !fill.read_context
194 {
195 let paths: Vec<_> = dir_entries.iter().map(|d| d.path()).collect();
196 if let Some(entries) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill) {
197 if entries.len() * 10 >= paths.len() * 8 {
199 return entries;
200 }
201 }
202 }
203 }
204
205 if !opts.use_parallel_stat(dir_entries.len()) {
206 return dir_entries.iter().filter_map(map_one).collect();
207 }
208
209 let collect = || dir_entries.par_iter().filter_map(map_one).collect();
210
211 if opts.threads > 1 {
212 match rayon::ThreadPoolBuilder::new()
213 .num_threads(opts.threads)
214 .build()
215 {
216 Ok(pool) => pool.install(collect),
217 Err(_) => collect(),
218 }
219 } else {
220 collect()
221 }
222}
223
224pub fn list_directory(path: &Path, opts: &ListOptions) -> Result<Listing> {
226 let collect_timing = opts.collect_timing;
227 let mut timing = ListTiming::default();
228
229 let mut entries = Vec::new();
230
231 let fill = meta_fill_from(opts);
232 if opts.all {
234 if let Ok(dot) = Entry::from_path_with(path, 0, fill) {
235 let mut e = dot;
236 e.name = ".".to_string();
237 entries.push(e);
238 }
239 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
240 if let Ok(mut e) = Entry::from_path_with(parent, 0, fill) {
241 e.name = "..".to_string();
242 e.path = parent.to_path_buf();
243 entries.push(e);
244 }
245 } else if let Ok(mut e) = Entry::from_path_with(path, 0, fill) {
246 e.name = "..".to_string();
248 entries.push(e);
249 }
250 }
251
252 let t_readdir = if collect_timing {
253 Some(Instant::now())
254 } else {
255 None
256 };
257
258 let read = fs::read_dir(path).map_err(|source| Error::ReadDir {
259 path: path.to_path_buf(),
260 source,
261 })?;
262
263 let mut dir_entries = Vec::new();
264 for item in read {
265 let item = item.map_err(|source| Error::ReadDir {
266 path: path.to_path_buf(),
267 source,
268 })?;
269 dir_entries.push(item);
270 }
271
272 if let Some(t0) = t_readdir {
273 timing.readdir_ms = t0.elapsed().as_millis();
274 }
275
276 let t_stat = if collect_timing {
277 Some(Instant::now())
278 } else {
279 None
280 };
281
282 let children = stat_dir_entries(&dir_entries, opts);
283 entries.extend(children);
284
285 if let Some(t0) = t_stat {
286 timing.stat_ms = t0.elapsed().as_millis();
287 }
288
289 let t_sort = if collect_timing {
290 Some(Instant::now())
291 } else {
292 None
293 };
294
295 filter_entries(&mut entries, opts);
296 if opts.use_ignore_files {
297 let set = load_ignore_set(path);
298 apply_ignore_set(&mut entries, &set);
299 }
300 sort_entries(&mut entries, opts);
302
303 if let Some(t0) = t_sort {
304 timing.sort_ms = t0.elapsed().as_millis();
305 }
306
307 Ok(
308 Listing::new(path.to_path_buf(), true, entries).with_timing(if collect_timing {
309 Some(timing)
310 } else {
311 None
312 }),
313 )
314}
315
316struct Walked {
318 path: PathBuf,
319 depth: usize,
320 is_dir: bool,
321}
322
323pub fn list_recursive(path: &Path, opts: &ListOptions) -> Result<Listing> {
325 let collect_timing = opts.collect_timing;
326 let mut timing = ListTiming::default();
327
328 let mut minor_errors = 0usize;
329 let max_depth = opts.max_depth.unwrap_or(usize::MAX);
330
331 let root_ignore = if opts.use_ignore_files {
332 Some(load_ignore_set(path))
333 } else {
334 None
335 };
336
337 let t_walk = if collect_timing {
338 Some(Instant::now())
339 } else {
340 None
341 };
342
343 let mut walked: Vec<Walked> = Vec::new();
346 let mut jwalker = jwalk::WalkDir::new(path)
348 .follow_links(opts.follow_links)
349 .skip_hidden(false)
350 .sort(true);
351 if max_depth != usize::MAX {
352 jwalker = jwalker.max_depth(max_depth);
353 }
354
355 for item in jwalker {
356 let item = match item {
357 Ok(i) => i,
358 Err(_) => {
359 minor_errors += 1;
360 continue;
361 }
362 };
363 let depth = item.depth();
364 if depth == 0 {
365 continue; }
367 let is_dir = item.file_type().is_dir();
368 walked.push(Walked {
369 path: item.path(),
370 depth,
371 is_dir,
372 });
373 }
374
375 for w in &walked {
378 if w.is_dir {
379 if let Err(err) = fs::read_dir(&w.path) {
380 let kind = err.kind();
381 if matches!(
382 kind,
383 std::io::ErrorKind::PermissionDenied
384 | std::io::ErrorKind::NotFound
385 | std::io::ErrorKind::Other
386 ) {
387 minor_errors += 1;
388 }
389 }
390 }
391 }
392
393 if let Some(t0) = t_walk {
394 timing.readdir_ms = t0.elapsed().as_millis();
395 }
396
397 let fill = meta_fill_from(opts);
398 let t_stat = if collect_timing {
399 Some(Instant::now())
400 } else {
401 None
402 };
403
404 let built: Vec<Option<Entry>> = {
407 #[cfg(all(target_os = "linux", feature = "io-uring"))]
408 {
409 if opts.io_uring
410 && !opts.follow_links
411 && walked.len() >= crate::io_uring_stat::IO_URING_THRESHOLD
412 && !fill.resolve_names
413 && !fill.read_context
414 {
415 let paths: Vec<_> = walked.iter().map(|w| w.path.clone()).collect();
416 if let Some(mut ents) = crate::io_uring_stat::entries_from_paths_uring(&paths, fill)
417 {
418 if ents.len() * 10 >= paths.len() * 8 {
419 use std::collections::HashMap;
421 let depth_by: HashMap<&Path, usize> =
422 walked.iter().map(|w| (w.path.as_path(), w.depth)).collect();
423 for e in &mut ents {
424 if let Some(&d) = depth_by.get(e.path.as_path()) {
425 e.depth = d;
426 }
427 }
428 let mut by_path: HashMap<PathBuf, Entry> =
430 ents.into_iter().map(|e| (e.path.clone(), e)).collect();
431 let ordered: Vec<Option<Entry>> = walked
432 .iter()
433 .map(|w| {
434 by_path
435 .remove(&w.path)
436 .filter(|e| crate::filter::should_show(e, opts))
437 })
438 .collect();
439 if let Some(t0) = t_stat {
440 timing.stat_ms = t0.elapsed().as_millis();
441 }
442 return finish_recursive(
444 path,
445 opts,
446 walked,
447 ordered,
448 root_ignore,
449 minor_errors,
450 timing,
451 collect_timing,
452 );
453 }
454 }
455 }
456 }
457
458 let use_parallel = opts.use_parallel_stat(walked.len().max(1));
459 let map = |w: &Walked| {
460 Entry::from_path_with(&w.path, w.depth, fill)
461 .ok()
462 .filter(|e| crate::filter::should_show(e, opts))
463 };
464 if use_parallel {
465 if opts.threads > 1 {
466 match rayon::ThreadPoolBuilder::new()
467 .num_threads(opts.threads)
468 .build()
469 {
470 Ok(pool) => pool.install(|| walked.par_iter().map(map).collect()),
471 Err(_) => walked.par_iter().map(map).collect(),
472 }
473 } else {
474 walked.par_iter().map(map).collect()
475 }
476 } else {
477 walked.iter().map(map).collect()
478 }
479 };
480
481 if let Some(t0) = t_stat {
482 timing.stat_ms = t0.elapsed().as_millis();
483 }
484
485 finish_recursive(
486 path,
487 opts,
488 walked,
489 built,
490 root_ignore,
491 minor_errors,
492 timing,
493 collect_timing,
494 )
495}
496
497#[allow(clippy::too_many_arguments)]
498fn finish_recursive(
499 path: &Path,
500 opts: &ListOptions,
501 walked: Vec<Walked>,
502 built: Vec<Option<Entry>>,
503 root_ignore: Option<IgnoreSet>,
504 minor_errors: usize,
505 mut timing: ListTiming,
506 collect_timing: bool,
507) -> Result<Listing> {
508 let mut ignore_by_dir: Vec<(PathBuf, IgnoreSet)> = Vec::new();
512 let mut plain: Vec<Entry> = Vec::with_capacity(built.len());
513
514 for (_w, maybe) in walked.iter().zip(built) {
515 let Some(e) = maybe else {
516 continue;
517 };
518 if ignored_by_sets(&e, opts, root_ignore.as_ref(), &mut ignore_by_dir) {
519 continue;
520 }
521 plain.push(e);
522 }
523
524 let t_sort = if collect_timing {
525 Some(Instant::now())
526 } else {
527 None
528 };
529
530 let entries = if opts.emit_dir_headers {
531 rebuild_gnu_recursive_sections(path, plain, opts)
532 } else {
533 let mut entries = plain;
536 if !matches!(opts.sort_by, crate::options::SortBy::Name) || opts.reverse {
537 sort_tree_preorder(&mut entries, opts);
538 }
539 entries
540 };
541
542 if let Some(t0) = t_sort {
543 timing.sort_ms = t0.elapsed().as_millis();
544 }
545
546 let mut listing = Listing::new(path.to_path_buf(), true, entries)
547 .with_timing(if collect_timing { Some(timing) } else { None });
548 listing.minor_errors = minor_errors;
549 Ok(listing)
550}
551
552fn sort_tree_preorder(entries: &mut [Entry], opts: &ListOptions) {
556 if entries.is_empty() {
557 return;
558 }
559 let mut i = 0;
564 while i < entries.len() {
565 let depth = entries[i].depth;
566 let parent = entries[i].path.parent().map(Path::to_path_buf);
569 let mut j = i + 1;
570 while j < entries.len() {
571 if entries[j].depth < depth {
572 break;
573 }
574 if entries[j].depth == depth {
575 let p = entries[j].path.parent().map(Path::to_path_buf);
576 if p != parent {
577 break;
578 }
579 }
580 j += 1;
581 }
582 let sib: Vec<usize> = (i..j).filter(|&k| entries[k].depth == depth).collect();
584 if sib.len() > 1 {
585 let mut blocks: Vec<Vec<Entry>> = Vec::with_capacity(sib.len());
588 for (bi, &start) in sib.iter().enumerate() {
589 let end = sib.get(bi + 1).copied().unwrap_or(j);
590 blocks.push(entries[start..end].to_vec());
591 }
592 blocks.sort_by(|a, b| crate::sort::cmp_entry(&a[0], &b[0], opts));
594 let mut out = Vec::with_capacity(j - i);
595 for b in blocks {
596 out.extend(b);
597 }
598 entries[i..j].clone_from_slice(&out);
599 }
600 i = if j > i { j } else { i + 1 };
601 }
602}
603
604fn ignored_by_sets(
606 entry: &Entry,
607 opts: &ListOptions,
608 root_ignore: Option<&IgnoreSet>,
609 cache: &mut Vec<(PathBuf, IgnoreSet)>,
610) -> bool {
611 if !opts.use_ignore_files {
612 return false;
613 }
614 if let Some(root) = root_ignore {
615 if root.ignores(entry) {
616 return true;
617 }
618 }
619 let Some(parent) = entry.path.parent() else {
620 return false;
621 };
622 if let Some(root) = root_ignore {
623 if parent == root.base {
624 return false;
625 }
626 }
627 if let Some((_, set)) = cache.iter().find(|(p, _)| p == parent) {
628 return set.ignores(entry);
629 }
630 let set = load_ignore_set(parent);
631 let ignored = set.ignores(entry);
632 cache.push((parent.to_path_buf(), set));
633 ignored
634}
635
636fn rebuild_gnu_recursive_sections(
639 root: &Path,
640 plain: Vec<Entry>,
641 opts: &ListOptions,
642) -> Vec<Entry> {
643 use std::collections::HashMap;
644
645 let mut by_parent: HashMap<PathBuf, Vec<Entry>> = HashMap::new();
646 for e in plain {
647 let parent = e
648 .path
649 .parent()
650 .map(Path::to_path_buf)
651 .unwrap_or_else(|| root.to_path_buf());
652 by_parent.entry(parent).or_default().push(e);
653 }
654
655 let mut out = Vec::new();
656 fn emit(
657 dir: &Path,
658 depth: usize,
659 by_parent: &mut HashMap<PathBuf, Vec<Entry>>,
660 opts: &ListOptions,
661 out: &mut Vec<Entry>,
662 ) {
663 out.push(Entry::dir_header(dir, depth));
664 let mut kids = by_parent.remove(dir).unwrap_or_default();
665 sort_entries(&mut kids, opts);
666 let subdirs: Vec<PathBuf> = kids
667 .iter()
668 .filter(|e| e.kind == EntryKind::Directory)
669 .map(|e| e.path.clone())
670 .collect();
671 out.extend(kids);
672 for sub in subdirs {
673 emit(&sub, depth.saturating_add(1), by_parent, opts, out);
674 }
675 }
676 emit(root, 0, &mut by_parent, opts, &mut out);
677 out
678}
679
680#[derive(Debug)]
682pub struct ListOutcome {
683 pub listings: Vec<Listing>,
684 pub path_errors: Vec<Error>,
686 pub minor_errors: usize,
688}
689
690impl ListOutcome {
691 pub fn exit_code(&self) -> i32 {
693 if !self.path_errors.is_empty() {
694 2
695 } else if self.minor_errors > 0 || self.listings.iter().any(|l| l.minor_errors > 0) {
696 1
697 } else {
698 0
699 }
700 }
701
702 pub fn total_timing(&self) -> ListTiming {
704 let mut total = ListTiming::default();
705 for l in &self.listings {
706 if let Some(ref t) = l.timing {
707 total.add_assign(t);
708 }
709 }
710 total
711 }
712}
713
714pub fn list_paths(paths: &[PathBuf], opts: &ListOptions) -> Result<Vec<Listing>> {
719 let outcome = list_paths_with_errors(paths, opts);
720 if let Some(err) = outcome.path_errors.into_iter().next() {
721 return Err(err);
723 }
724 Ok(outcome.listings)
725}
726
727pub fn list_paths_with_errors(paths: &[PathBuf], opts: &ListOptions) -> ListOutcome {
729 let owned: Vec<PathBuf>;
730 let paths: &[PathBuf] = if paths.is_empty() {
731 owned = vec![PathBuf::from(".")];
732 &owned
733 } else {
734 paths
735 };
736
737 let mut listings = Vec::with_capacity(paths.len());
738 let mut path_errors = Vec::new();
739 let mut minor_errors = 0usize;
740
741 for p in paths {
742 match list_path(p, opts) {
743 Ok(l) => {
744 minor_errors += l.minor_errors;
745 listings.push(l);
746 }
747 Err(e) => path_errors.push(e),
748 }
749 }
750
751 ListOutcome {
752 listings,
753 path_errors,
754 minor_errors,
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use std::fs;
762
763 fn temp_dir() -> PathBuf {
764 use std::sync::atomic::{AtomicU64, Ordering};
765 static N: AtomicU64 = AtomicU64::new(0);
766 let base = std::env::temp_dir().join(format!(
767 "f00-core-list-{}-{}-{}",
768 std::process::id(),
769 std::time::SystemTime::now()
770 .duration_since(std::time::UNIX_EPOCH)
771 .map(|d| d.as_nanos())
772 .unwrap_or(0),
773 N.fetch_add(1, Ordering::Relaxed)
774 ));
775 fs::create_dir_all(&base).unwrap();
776 base
777 }
778
779 #[test]
780 fn list_paths_with_errors_partial_success() {
781 let dir = temp_dir();
782 fs::write(dir.join("a.txt"), b"x").unwrap();
783 let missing = dir.join("gone");
784 let opts = ListOptions::default();
785 let outcome = list_paths_with_errors(&[dir.clone(), missing], &opts);
786 assert_eq!(outcome.listings.len(), 1);
787 assert_eq!(outcome.path_errors.len(), 1);
788 assert_eq!(outcome.exit_code(), 2);
789 let _ = fs::remove_dir_all(&dir);
790 }
791
792 #[test]
793 fn list_paths_ok_exit_0() {
794 let dir = temp_dir();
795 fs::write(dir.join("a.txt"), b"x").unwrap();
796 let opts = ListOptions::default();
797 let outcome = list_paths_with_errors(std::slice::from_ref(&dir), &opts);
798 assert!(outcome.path_errors.is_empty());
799 assert_eq!(outcome.exit_code(), 0);
800 let _ = fs::remove_dir_all(&dir);
801 }
802
803 #[test]
804 fn list_directory_honors_ignore_files() {
805 let dir = temp_dir();
806 fs::write(dir.join("keep.txt"), b"k").unwrap();
807 fs::write(dir.join("skip.o"), b"o").unwrap();
808 fs::write(dir.join(".gitignore"), "*.o\n").unwrap();
809
810 let opts = ListOptions {
811 use_ignore_files: true,
812 ..Default::default()
813 };
814 let listing = list_directory(&dir, &opts).unwrap();
815 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
816 assert!(names.contains(&"keep.txt"));
817 assert!(!names.contains(&"skip.o"));
818
819 let opts_off = ListOptions {
820 use_ignore_files: false,
821 ..Default::default()
822 };
823 let listing = list_directory(&dir, &opts_off).unwrap();
824 let names: Vec<_> = listing.entries.iter().map(|e| e.name.as_str()).collect();
825 assert!(names.contains(&"skip.o"));
826
827 let _ = fs::remove_dir_all(&dir);
828 }
829
830 #[test]
831 fn parallel_list_same_names_as_sequential() {
832 let dir = temp_dir();
833 let mut expected = Vec::new();
835 for i in 0..64 {
836 let name = format!("file_{i:03}.txt");
837 fs::write(dir.join(&name), b"x").unwrap();
838 expected.push(name);
839 }
840 fs::create_dir(dir.join("subdir")).unwrap();
841 expected.push("subdir".into());
842 expected.sort();
843
844 let sequential = ListOptions {
845 parallel: false,
846 threads: 1,
847 ..Default::default()
848 };
849 let seq = list_directory(&dir, &sequential).unwrap();
850 let mut seq_names: Vec<_> = seq.entries.iter().map(|e| e.name.clone()).collect();
851 seq_names.sort();
852 assert_eq!(
853 seq_names, expected,
854 "listing must include every created entry"
855 );
856
857 if !cfg!(target_os = "freebsd") {
859 let parallel = ListOptions {
860 parallel: true,
861 threads: 0,
862 ..Default::default()
863 };
864 let par = list_directory(&dir, ¶llel).unwrap();
865 let mut par_names: Vec<_> = par.entries.iter().map(|e| e.name.clone()).collect();
866 par_names.sort();
867 assert_eq!(
868 seq_names, par_names,
869 "parallel and sequential must produce identical ordered names"
870 );
871 }
872
873 let _ = fs::remove_dir_all(&dir);
874 }
875
876 #[test]
877 fn collect_timing_fills_phases() {
878 let dir = temp_dir();
879 for i in 0..8 {
880 fs::write(dir.join(format!("f{i}")), b"x").unwrap();
881 }
882 let opts = ListOptions {
883 collect_timing: true,
884 parallel: false,
885 threads: 1,
886 ..Default::default()
887 };
888 let listing = list_directory(&dir, &opts).unwrap();
889 let t = listing.timing.expect("timing present");
890 let _ = (t.readdir_ms, t.stat_ms, t.sort_ms);
892 let _ = fs::remove_dir_all(&dir);
893 }
894
895 #[test]
896 fn threads_one_forces_serial_path() {
897 let dir = temp_dir();
898 for i in 0..40 {
899 fs::write(dir.join(format!("n{i}")), b"x").unwrap();
900 }
901 let opts = ListOptions {
902 parallel: true,
903 threads: 1,
904 ..Default::default()
905 };
906 assert!(!opts.use_parallel_stat(40));
907 let listing = list_directory(&dir, &opts).unwrap();
908 assert_eq!(listing.entries.len(), 40);
909 let _ = fs::remove_dir_all(&dir);
910 }
911}