1#![deny(unsafe_code)]
24#![deny(missing_docs)]
25
26use crossbeam::{
27 deque::{Injector, Steal, Stealer, Worker},
28 sync::{Parker, Unparker},
29};
30use std::{
31 collections::HashMap,
32 io,
33 path::{Path, PathBuf},
34 sync::{
35 Arc,
36 atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
37 mpsc::{Receiver, SyncSender, sync_channel},
38 },
39 thread,
40};
41
42#[cfg(any(not(windows), test))]
43use std::{ffi::OsString, fs};
44
45#[cfg(not(windows))]
46pub use std::fs::{FileType, Metadata};
47
48#[cfg(windows)]
49#[allow(unsafe_code)]
50mod windows;
51
52#[cfg(windows)]
53pub use windows::{Entry, FileType, Metadata};
54
55type Descend = dyn Fn(usize, &Entry) -> bool + Send + Sync;
58type Batch = io::Result<Vec<io::Result<Entry>>>;
62const ENTRY_CHUNK_SIZE: usize = 4;
65
66#[derive(Clone, Copy)]
68pub enum Order {
69 Completion,
71 ParentFirst,
73}
74
75#[cfg(not(windows))]
77pub struct Entry {
78 pub depth: usize,
80 pub file_name: OsString,
82 pub file_type: FileType,
84 pub metadata: io::Result<Metadata>,
86 pub parent_path: Arc<Path>,
88}
89
90enum Job {
91 ReadDir {
93 root_idx: usize,
94 path: Arc<Path>,
95 entry_depth: usize,
98 },
99 #[cfg(not(windows))]
101 StatCompletion {
102 root_idx: usize,
103 path: Arc<Path>,
104 entry_depth: usize,
106 entries: Vec<fs::DirEntry>,
107 },
108}
109
110impl Job {
111 fn root_idx(&self) -> usize {
113 match self {
114 Job::ReadDir { root_idx, .. } => *root_idx,
115 #[cfg(not(windows))]
116 Job::StatCompletion { root_idx, .. } => *root_idx,
117 }
118 }
119}
120
121enum Event {
123 Batch {
124 root_idx: usize,
125 batch: Batch,
126 },
127 RootFinished {
130 root_idx: usize,
131 },
132 Finished,
134}
135
136pub enum RootEvent {
142 Entry(io::Result<Entry>),
144 Finished,
146}
147
148struct PoolShared {
149 injector: Injector<Job>,
151 stealers: Vec<Stealer<Job>>,
152 stop: AtomicBool,
153 descend: Arc<Descend>,
154 events: SyncSender<Event>,
155 active_roots: AtomicUsize,
157 jobs_per_root: HashMap<usize, AtomicUsize>,
160 order: Order,
161 unparkers: Vec<Unparker>,
163 idle: Vec<AtomicBool>,
166 next_wake: AtomicUsize,
168}
169
170struct Pool {
171 shared: Arc<PoolShared>,
172 events: Receiver<Event>,
173 handles: Vec<thread::JoinHandle<()>>,
174}
175
176pub struct RootWalk {
179 next: Vec<(usize, RootEvent)>,
181 pool: Option<Pool>,
183}
184
185pub struct Walk {
188 next: Vec<io::Result<Entry>>,
195 pool: Option<Pool>,
199}
200
201pub fn walk(
205 root: &Path,
206 threads: usize,
207 order: Order,
208 descend: impl Fn(&Entry) -> bool + Send + Sync + 'static,
209) -> Walk {
210 let root = Entry::from_path(root);
211 let pool = match &root {
212 Ok(entry) if entry.file_type.is_dir() && descend(entry) => {
213 let path = Arc::from(entry.path());
214 let pool = start_pool(
215 threads.max(1),
216 HashMap::from([(0, AtomicUsize::new(0))]),
217 order,
218 Arc::new(move |_, entry| descend(entry)),
219 );
220 start_jobs(
221 &pool,
222 vec![Job::ReadDir {
223 root_idx: 0,
224 path,
225 entry_depth: 1,
226 }],
227 );
228 Some(pool)
229 }
230 _ => None,
231 };
232 Walk {
233 next: vec![root],
234 pool,
235 }
236}
237
238impl Iterator for Walk {
239 type Item = io::Result<Entry>;
240
241 fn next(&mut self) -> Option<Self::Item> {
242 loop {
243 if let Some(entry) = self.next.pop() {
244 return Some(entry);
245 }
246
247 match self.pool.as_ref()?.events.recv() {
248 Ok(Event::Batch {
249 batch: Ok(entries), ..
250 }) => {
251 self.next.extend(entries.into_iter().rev());
252 }
253 Ok(Event::Batch {
254 batch: Err(err), ..
255 }) => return Some(Err(err)),
256 Ok(Event::RootFinished { .. }) => {}
257 Ok(Event::Finished) => {
258 self.pool = None;
259 return None;
260 }
261 Err(_) => return Some(Err(io::Error::other("directory worker stopped"))),
262 }
263 }
264 }
265}
266
267pub fn walk_roots(
274 roots: impl IntoIterator<Item = (usize, PathBuf)>,
275 threads: usize,
276 order: Order,
277 descend: impl Fn(usize, &Entry) -> bool + Send + Sync + 'static,
278) -> RootWalk {
279 let roots = roots.into_iter().collect::<Vec<_>>();
280 let jobs_per_root = roots
281 .iter()
282 .map(|(root_idx, _)| (*root_idx, AtomicUsize::new(0)))
283 .collect::<HashMap<_, _>>();
284 assert_eq!(
285 jobs_per_root.len(),
286 roots.len(),
287 "root indices must be unique"
288 );
289 let descend = Arc::new(descend);
290 let (next, root_jobs) = begin_walks(roots, descend.as_ref());
291 let pool = if root_jobs.is_empty() {
292 None
293 } else {
294 let pool = start_pool(threads.max(1), jobs_per_root, order, descend);
295 start_jobs(&pool, root_jobs);
296 Some(pool)
297 };
298 RootWalk { next, pool }
299}
300
301impl Iterator for RootWalk {
302 type Item = (usize, RootEvent);
303
304 fn next(&mut self) -> Option<Self::Item> {
305 loop {
306 if let Some(entry) = self.next.pop() {
307 return Some(entry);
308 }
309 match self.pool.as_ref()?.events.recv() {
310 Ok(Event::Batch {
311 root_idx,
312 batch: Ok(entries),
313 }) => self.next.extend(
314 entries
315 .into_iter()
316 .rev()
317 .map(|entry| (root_idx, RootEvent::Entry(entry))),
318 ),
319 Ok(Event::Batch {
320 root_idx,
321 batch: Err(err),
322 }) => return Some((root_idx, RootEvent::Entry(Err(err)))),
323 Ok(Event::RootFinished { root_idx }) => {
324 return Some((root_idx, RootEvent::Finished));
325 }
326 Ok(Event::Finished) => {
327 self.pool = None;
328 return None;
329 }
330 Err(_) => {
331 return Some((
332 0,
333 RootEvent::Entry(Err(io::Error::other("directory worker stopped"))),
334 ));
335 }
336 }
337 }
338 }
339}
340
341impl PoolShared {
342 fn wake_worker(&self) {
344 let len = self.idle.len();
345 let start = self.next_wake.fetch_add(1, AtomicOrdering::Relaxed) % len;
348 for offset in 0..len {
349 let idx = (start + offset) % len;
350 if self.idle[idx]
351 .compare_exchange(true, false, AtomicOrdering::AcqRel, AtomicOrdering::Relaxed)
352 .is_ok()
353 {
354 self.unparkers[idx].unpark();
355 break;
356 }
357 }
358 }
359
360 fn wake_workers(&self) {
362 for unparker in &self.unparkers {
363 unparker.unpark();
364 }
365 }
366}
367
368#[cfg(not(windows))]
369impl Entry {
370 #[must_use]
372 pub fn path(&self) -> PathBuf {
373 self.parent_path.join(&self.file_name)
374 }
375
376 pub fn from_path(path: &Path) -> io::Result<Self> {
378 let metadata = fs::symlink_metadata(path)?;
379 Ok(Self {
380 depth: 0,
381 file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
382 file_type: metadata.file_type(),
383 metadata: Ok(metadata),
384 parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
385 })
386 }
387
388 fn from_dir_entry(
389 depth: usize,
390 parent_path: Arc<Path>,
391 entry: fs::DirEntry,
392 ) -> io::Result<Self> {
393 Ok(Self {
394 depth,
395 file_name: entry.file_name(),
396 file_type: entry.file_type()?,
397 metadata: entry.metadata(),
398 parent_path,
399 })
400 }
401}
402
403fn start_pool(
404 threads: usize,
405 jobs_per_root: HashMap<usize, AtomicUsize>,
406 order: Order,
407 descend: Arc<Descend>,
408) -> Pool {
409 let workers: Vec<_> = (0..threads).map(|_| Worker::new_lifo()).collect();
410 let parkers: Vec<_> = (0..threads).map(|_| Parker::new()).collect();
411 let (event_tx, event_rx) = sync_channel(threads * 2);
412 let shared = Arc::new(PoolShared {
413 injector: Injector::new(),
414 stealers: workers.iter().map(Worker::stealer).collect(),
415 stop: AtomicBool::new(false),
416 descend,
417 events: event_tx,
418 active_roots: AtomicUsize::new(0),
419 jobs_per_root,
420 order,
421 unparkers: parkers
422 .iter()
423 .map(|parker| parker.unparker().clone())
424 .collect(),
425 idle: (0..threads).map(|_| AtomicBool::new(false)).collect(),
426 next_wake: AtomicUsize::new(0),
427 });
428 let handles: Vec<_> = workers
429 .into_iter()
430 .zip(parkers)
431 .enumerate()
432 .map(|(idx, (worker, parker))| {
433 let shared = Arc::clone(&shared);
434 thread::Builder::new()
435 .name(format!("dua-fs-walk-{idx}"))
436 .spawn(move || worker_loop(idx, worker, parker, shared))
437 .expect("filesystem worker thread can be spawned")
438 })
439 .collect();
440
441 Pool {
442 shared,
443 events: event_rx,
444 handles,
445 }
446}
447
448fn begin_walks(
451 roots: impl IntoIterator<Item = (usize, PathBuf)>,
452 descend: &Descend,
453) -> (Vec<(usize, RootEvent)>, Vec<Job>) {
454 let mut next = Vec::new();
455 let mut jobs = Vec::new();
456 for (root_idx, path) in roots {
457 let entry = Entry::from_path(&path);
458 let has_job = if let Ok(entry) = &entry
459 && entry.file_type.is_dir()
460 && descend(root_idx, entry)
461 {
462 jobs.push(Job::ReadDir {
463 root_idx,
464 path: Arc::from(entry.path()),
465 entry_depth: 1,
466 });
467 true
468 } else {
469 false
470 };
471 next.push((root_idx, RootEvent::Entry(entry)));
472 if !has_job {
473 next.push((root_idx, RootEvent::Finished));
474 }
475 }
476 next.reverse();
477 (next, jobs)
478}
479
480fn start_jobs(pool: &Pool, root_jobs: Vec<Job>) {
483 let wake_all = root_jobs.len() > 1;
484 debug_assert_eq!(
485 pool.shared.active_roots.load(AtomicOrdering::Relaxed),
486 0,
487 "initial jobs must be started on an idle pool"
488 );
489 debug_assert!(
490 root_jobs.iter().all(|j| match j {
491 Job::ReadDir { entry_depth, .. } => *entry_depth,
492 #[cfg(not(windows))]
493 Job::StatCompletion { entry_depth, .. } => *entry_depth,
494 } == 1),
495 "the first jobs should be root jobs, so active_root counts match"
496 );
497 pool.shared
498 .active_roots
499 .store(root_jobs.len(), AtomicOrdering::Relaxed);
500 for job in &root_jobs {
501 add_pending(job.root_idx(), 1, &pool.shared);
502 }
503 for job in root_jobs {
504 pool.shared.injector.push(job);
505 }
506 if wake_all {
507 pool.shared.wake_workers();
508 } else {
509 pool.shared.wake_worker();
510 }
511}
512
513fn worker_loop(idx: usize, worker: Worker<Job>, parker: Parker, shared: Arc<PoolShared>) {
514 while !shared.stop.load(AtomicOrdering::Relaxed) {
515 let found = if let Some(found) = find_job(&worker, &shared) {
516 found
517 } else {
518 shared.idle[idx].store(true, AtomicOrdering::Release);
519 let Some(found) = find_job(&worker, &shared) else {
520 parker.park();
521 shared.idle[idx].store(false, AtomicOrdering::Release);
522 continue;
523 };
524 shared.idle[idx].store(false, AtomicOrdering::Release);
525 found
526 };
527 let (job, stolen) = found;
528 if stolen {
529 shared.wake_worker();
532 }
533 run_job(job, &worker, &shared);
534 }
535}
536
537impl Drop for Pool {
538 fn drop(&mut self) {
539 self.shared.stop.store(true, AtomicOrdering::Relaxed);
540 self.shared.wake_workers();
541 for handle in self.handles.drain(..) {
542 handle.join().ok();
543 }
544 }
545}
546
547fn find_job(worker: &Worker<Job>, shared: &PoolShared) -> Option<(Job, bool)> {
558 loop {
559 if let Some(job) = worker.pop() {
560 return Some((job, false));
561 }
562
563 match shared.injector.steal_batch_and_pop(worker) {
564 Steal::Success(job) => return Some((job, false)),
565 Steal::Retry => continue,
566 Steal::Empty => {}
567 }
568
569 let mut retry = false;
570 for stealer in &shared.stealers {
571 match stealer.steal() {
572 Steal::Success(job) => return Some((job, true)),
573 Steal::Retry => retry = true,
574 Steal::Empty => {}
575 }
576 }
577 if !retry {
578 return None;
579 }
580 }
581}
582
583fn run_job(job: Job, worker: &Worker<Job>, shared: &PoolShared) {
584 match job {
585 Job::ReadDir {
586 root_idx: root,
587 path,
588 entry_depth,
589 } => {
590 if matches!(shared.order, Order::Completion) {
591 read_dir_completion(root, path, entry_depth, worker, shared);
592 } else {
593 read_dir_parent_first(root, path, entry_depth, worker, shared);
594 }
595 }
596 #[cfg(not(windows))]
597 Job::StatCompletion {
598 root_idx: root,
599 path,
600 entry_depth,
601 entries,
602 } => stat_entries_completion(root, path, entry_depth, entries, worker, shared),
603 }
604}
605
606#[cfg(not(windows))]
612fn read_dir_completion(
613 root_idx: usize,
614 path: Arc<Path>,
615 entry_depth: usize,
616 worker: &Worker<Job>,
617 shared: &PoolShared,
618) {
619 let dir_entries = match fs::read_dir(&path) {
620 Ok(entries) => entries,
621 Err(err) => {
622 if shared
623 .events
624 .send(Event::Batch {
625 root_idx,
626 batch: Err(err),
627 })
628 .is_err()
629 {
630 shared.stop.store(true, AtomicOrdering::Relaxed);
631 }
632 finish_pending(root_idx, shared);
633 return;
634 }
635 };
636 let mut chunk = Vec::with_capacity(ENTRY_CHUNK_SIZE);
637 let mut errors = Vec::new();
638 let mut has_jobs = false;
639 for entry in dir_entries {
640 match entry {
641 Ok(entry) => {
642 chunk.push(entry);
643 if chunk.len() == ENTRY_CHUNK_SIZE {
644 add_pending(root_idx, 1, shared);
645 worker.push(Job::StatCompletion {
646 root_idx,
647 path: Arc::clone(&path),
648 entry_depth,
649 entries: std::mem::replace(
650 &mut chunk,
651 Vec::with_capacity(ENTRY_CHUNK_SIZE),
652 ),
653 });
654 has_jobs = true;
655 }
656 }
657 Err(err) => errors.push(Err(err)),
658 }
659 }
660 if !chunk.is_empty() {
661 add_pending(root_idx, 1, shared);
662 worker.push(Job::StatCompletion {
663 root_idx,
664 path,
665 entry_depth,
666 entries: chunk,
667 });
668 has_jobs = true;
669 }
670 if has_jobs {
671 shared.wake_worker();
672 }
673 if !errors.is_empty()
674 && shared
675 .events
676 .send(Event::Batch {
677 root_idx,
678 batch: Ok(errors),
679 })
680 .is_err()
681 {
682 shared.stop.store(true, AtomicOrdering::Relaxed);
683 }
684 finish_pending(root_idx, shared);
685}
686
687#[cfg(windows)]
693fn read_dir_completion(
694 root_idx: usize,
695 path: Arc<Path>,
696 depth: usize,
697 worker: &Worker<Job>,
698 shared: &PoolShared,
699) {
700 let dir_entries = match windows::ReadDir::open(path, depth) {
701 Ok(entries) => entries,
702 Err(err) => {
703 if shared
704 .events
705 .send(Event::Batch {
706 root_idx,
707 batch: Err(err),
708 })
709 .is_err()
710 {
711 shared.stop.store(true, AtomicOrdering::Relaxed);
712 }
713 finish_pending(root_idx, shared);
714 return;
715 }
716 };
717 let mut entries = Vec::with_capacity(ENTRY_CHUNK_SIZE);
718 let mut jobs = Vec::new();
719 for entry in dir_entries {
720 if let Ok(entry) = &entry
721 && entry.file_type.is_dir()
722 && (shared.descend)(root_idx, entry)
723 {
724 jobs.push(Job::ReadDir {
725 root_idx,
726 path: Arc::from(entry.path()),
727 entry_depth: depth + 1,
728 });
729 }
730 entries.push(entry);
731 if entries.len() == ENTRY_CHUNK_SIZE
732 && !publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared)
733 {
734 finish_pending(root_idx, shared);
735 return;
736 }
737 }
738 if !entries.is_empty() {
739 publish_completion_batch(root_idx, &mut entries, &mut jobs, worker, shared);
740 }
741 finish_pending(root_idx, shared);
742}
743
744#[cfg(windows)]
745fn publish_completion_batch(
746 root_idx: usize,
747 entries: &mut Vec<io::Result<Entry>>,
748 jobs: &mut Vec<Job>,
749 worker: &Worker<Job>,
750 shared: &PoolShared,
751) -> bool {
752 add_pending(root_idx, jobs.len(), shared);
753 schedule_jobs(std::mem::take(jobs), worker, shared);
754 if shared
755 .events
756 .send(Event::Batch {
757 root_idx,
758 batch: Ok(std::mem::replace(
759 entries,
760 Vec::with_capacity(ENTRY_CHUNK_SIZE),
761 )),
762 })
763 .is_err()
764 {
765 shared.stop.store(true, AtomicOrdering::Relaxed);
766 false
767 } else {
768 true
769 }
770}
771
772#[cfg(windows)]
773fn read_dir_parent_first(
774 root_idx: usize,
775 path: Arc<Path>,
776 depth: usize,
777 worker: &Worker<Job>,
778 shared: &PoolShared,
779) {
780 let dir_entries = match windows::ReadDir::open(path, depth) {
781 Ok(entries) => entries,
782 Err(err) => {
783 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
784 return;
785 }
786 };
787 let mut jobs = Vec::new();
788 let entries = dir_entries
789 .map(|entry| {
790 entry.inspect(|entry| {
791 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
792 jobs.push(Job::ReadDir {
793 root_idx,
794 path: Arc::from(entry.path()),
795 entry_depth: depth + 1,
796 });
797 }
798 })
799 })
800 .collect();
801 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
802}
803
804#[cfg(not(windows))]
805fn stat_entries_completion(
806 root_idx: usize,
807 path: Arc<Path>,
808 depth: usize,
809 entries: Vec<fs::DirEntry>,
810 worker: &Worker<Job>,
811 shared: &PoolShared,
812) {
813 let mut jobs = Vec::new();
814 let entries = entries
815 .into_iter()
816 .map(|entry| {
817 Entry::from_dir_entry(depth, Arc::clone(&path), entry).inspect(|entry| {
818 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
819 jobs.push(Job::ReadDir {
820 root_idx,
821 path: Arc::from(entry.path()),
822 entry_depth: entry.depth + 1,
823 });
824 }
825 })
826 })
827 .collect();
828 add_pending(root_idx, jobs.len(), shared);
829 schedule_jobs(jobs, worker, shared);
830 if shared
831 .events
832 .send(Event::Batch {
833 root_idx,
834 batch: Ok(entries),
835 })
836 .is_err()
837 {
838 shared.stop.store(true, AtomicOrdering::Relaxed);
839 }
840 finish_pending(root_idx, shared);
841}
842
843#[cfg(not(windows))]
851fn read_dir_parent_first(
852 root_idx: usize,
853 path: Arc<Path>,
854 depth: usize,
855 worker: &Worker<Job>,
856 shared: &PoolShared,
857) {
858 read_dir_inline(root_idx, path, depth, worker, shared);
859}
860
861#[cfg(not(windows))]
865fn read_dir_inline(
866 root_idx: usize,
867 path: Arc<Path>,
868 depth: usize,
869 worker: &Worker<Job>,
870 shared: &PoolShared,
871) {
872 let dir_entries = match fs::read_dir(&path) {
873 Ok(entries) => entries,
874 Err(err) => {
875 finish_directory(root_idx, Err(err), Vec::new(), worker, shared);
876 return;
877 }
878 };
879 let mut jobs = Vec::new();
880 let entries = dir_entries
881 .map(|entry| {
882 entry
883 .and_then(|entry| Entry::from_dir_entry(depth, Arc::clone(&path), entry))
884 .inspect(|entry| {
885 if entry.file_type.is_dir() && (shared.descend)(root_idx, entry) {
886 jobs.push(Job::ReadDir {
887 root_idx,
888 path: Arc::from(entry.path()),
889 entry_depth: depth + 1,
890 });
891 }
892 })
893 })
894 .collect();
895 finish_directory(root_idx, Ok(entries), jobs, worker, shared);
896}
897
898fn finish_directory(
902 root_idx: usize,
903 batch: Batch,
904 jobs: Vec<Job>,
905 worker: &Worker<Job>,
906 shared: &PoolShared,
907) {
908 add_pending(root_idx, jobs.len(), shared);
909
910 match shared.order {
911 Order::ParentFirst => {
912 if shared
913 .events
914 .send(Event::Batch { root_idx, batch })
915 .is_err()
916 {
917 shared.stop.store(true, AtomicOrdering::Relaxed);
918 return;
919 }
920 schedule_jobs(jobs, worker, shared);
921 }
922 Order::Completion => {
923 schedule_jobs(jobs, worker, shared);
924 if shared
925 .events
926 .send(Event::Batch { root_idx, batch })
927 .is_err()
928 {
929 shared.stop.store(true, AtomicOrdering::Relaxed);
930 return;
931 }
932 }
933 }
934
935 finish_pending(root_idx, shared);
936}
937
938fn add_pending(root: usize, count: usize, shared: &PoolShared) {
939 shared.jobs_per_root[&root].fetch_add(count, AtomicOrdering::Relaxed);
940}
941
942fn finish_pending(root_idx: usize, shared: &PoolShared) {
945 if shared.jobs_per_root[&root_idx].fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
946 shared.events.send(Event::RootFinished { root_idx }).ok();
947 if shared.active_roots.fetch_sub(1, AtomicOrdering::Relaxed) == 1 {
948 shared.events.send(Event::Finished).ok();
949 }
950 }
951}
952
953fn schedule_jobs(jobs: Vec<Job>, worker: &Worker<Job>, shared: &PoolShared) {
954 let has_jobs = !jobs.is_empty();
955 for job in jobs {
956 worker.push(job);
957 }
958 if has_jobs {
959 shared.wake_worker();
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn parallel_walk_is_parent_first_and_does_not_follow_symlinks() {
969 let dir = tempfile::tempdir().unwrap();
970 fs::create_dir_all(dir.path().join("b/child")).unwrap();
971 fs::create_dir(dir.path().join("a")).unwrap();
972 fs::write(dir.path().join("b/child/file"), b"x").unwrap();
973
974 #[cfg(unix)]
975 std::os::unix::fs::symlink(dir.path().join("b"), dir.path().join("link")).unwrap();
976
977 #[cfg(unix)]
978 let expected = ["", "a", "b", "b/child", "b/child/file", "link"];
979 #[cfg(not(unix))]
980 let expected = ["", "a", "b", "b/child", "b/child/file"];
981 let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
982
983 for threads in [1, 4] {
984 let paths = walk(dir.path(), threads, Order::ParentFirst, |_| true)
985 .map(|entry| {
986 entry
987 .unwrap()
988 .path()
989 .strip_prefix(dir.path())
990 .unwrap()
991 .to_owned()
992 })
993 .collect::<Vec<_>>();
994 let mut sorted_paths = paths.clone();
995 sorted_paths.sort();
996 assert_eq!(
997 sorted_paths, expected,
998 "walk with {threads} threads should visit every expected path exactly once"
999 );
1000
1001 for path in paths.iter().filter(|path| path.components().count() > 1) {
1002 let parent = path.parent().unwrap();
1003 assert!(
1004 paths.iter().position(|path| path == parent)
1005 < paths.iter().position(|candidate| candidate == path),
1006 "parent {parent:?} should precede child {path:?} with {threads} threads; \
1007 traversal order: {paths:?}"
1008 );
1009 }
1010 }
1011 }
1012
1013 #[test]
1014 fn pruning_keeps_the_directory_and_missing_roots_are_errors() {
1015 let dir = tempfile::tempdir().unwrap();
1016 fs::create_dir_all(dir.path().join("skip/child")).unwrap();
1017
1018 let paths = walk(dir.path(), 2, Order::Completion, |entry| {
1019 entry.file_name != "skip"
1020 })
1021 .map(|entry| entry.unwrap().file_name)
1022 .collect::<Vec<_>>();
1023 assert_eq!(
1024 paths,
1025 vec![
1026 dir.path().file_name().unwrap().to_owned(),
1027 OsString::from("skip")
1028 ],
1029 "a pruned directory should be yielded without traversing its children"
1030 );
1031
1032 assert!(
1033 walk(&dir.path().join("missing"), 2, Order::Completion, |_| true)
1034 .next()
1035 .unwrap()
1036 .is_err(),
1037 "a missing root should be yielded as an I/O error"
1038 );
1039 }
1040
1041 #[test]
1042 fn concurrent_roots_keep_their_identity() {
1043 let dir = tempfile::tempdir().unwrap();
1044 let roots = [dir.path().join("a"), dir.path().join("b")];
1045 for root in &roots {
1046 fs::create_dir_all(root.join("child")).unwrap();
1047 }
1048
1049 let events = walk_roots(
1050 roots.iter().cloned().enumerate(),
1051 2,
1052 Order::Completion,
1053 |_, _| true,
1054 )
1055 .collect::<Vec<_>>();
1056 let mut paths = Vec::new();
1057 let mut last_entry = [0; 2];
1058 let mut finished = [None; 2];
1059 for (position, (root_idx, event)) in events.into_iter().enumerate() {
1060 match event {
1061 RootEvent::Entry(entry) => {
1062 last_entry[root_idx] = position;
1063 paths.push((
1064 root_idx,
1065 entry
1066 .unwrap()
1067 .path()
1068 .strip_prefix(&roots[root_idx])
1069 .unwrap()
1070 .to_owned(),
1071 ));
1072 }
1073 RootEvent::Finished => finished[root_idx] = Some(position),
1074 }
1075 }
1076 paths.sort();
1077 assert_eq!(
1078 paths,
1079 [
1080 (0, PathBuf::new()),
1081 (0, PathBuf::from("child")),
1082 (1, PathBuf::new()),
1083 (1, PathBuf::from("child")),
1084 ]
1085 );
1086 for root_idx in 0..roots.len() {
1087 assert!(
1088 last_entry[root_idx] < finished[root_idx].unwrap(),
1089 "root {root_idx} must finish after its last entry",
1090 );
1091 }
1092 }
1093
1094 #[test]
1095 fn wide_walk_wakes_multiple_idle_workers() {
1096 let dir = tempfile::tempdir().unwrap();
1097 for idx in 0..32 {
1098 fs::create_dir_all(dir.path().join(format!("{idx}/child"))).unwrap();
1099 }
1100
1101 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1102 let seen_threads = Arc::clone(&worker_threads);
1103 walk(dir.path(), 8, Order::Completion, move |entry| {
1104 if entry.depth == 1 {
1105 thread::sleep(std::time::Duration::from_millis(1));
1106 } else if entry.depth == 2 {
1107 seen_threads.lock().unwrap().insert(thread::current().id());
1108 thread::sleep(std::time::Duration::from_millis(10));
1109 }
1110 true
1111 })
1112 .for_each(drop);
1113
1114 assert!(
1115 worker_threads.lock().unwrap().len() >= 4,
1116 "a wide directory should engage more than the producer and one thief"
1117 );
1118 }
1119
1120 #[cfg(windows)]
1121 #[test]
1122 fn windows_metadata_is_collected_by_the_directory_worker() {
1123 let dir = tempfile::tempdir().unwrap();
1124 for idx in 0..32 {
1125 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1126 }
1127
1128 let worker_threads = Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
1129 let seen_threads = Arc::clone(&worker_threads);
1130 walk(dir.path(), 8, Order::Completion, move |entry| {
1131 if entry.depth == 1 {
1132 seen_threads.lock().unwrap().insert(thread::current().id());
1133 thread::sleep(std::time::Duration::from_millis(2));
1134 }
1135 true
1136 })
1137 .for_each(drop);
1138
1139 assert_eq!(
1140 worker_threads.lock().unwrap().len(),
1141 1,
1142 "Windows directory-entry metadata should stay on the enumerating worker"
1143 );
1144 }
1145
1146 #[cfg(windows)]
1147 #[test]
1148 fn windows_completion_streams_metadata_before_enumeration_finishes() {
1149 let dir = tempfile::tempdir().unwrap();
1150 for idx in 0..=ENTRY_CHUNK_SIZE {
1151 fs::create_dir(dir.path().join(idx.to_string())).unwrap();
1152 }
1153
1154 let (continue_tx, continue_rx) = std::sync::mpsc::sync_channel(0);
1155 let continue_rx = Arc::new(std::sync::Mutex::new(continue_rx));
1156 let seen = Arc::new(AtomicUsize::new(0));
1157 let seen_in_worker = Arc::clone(&seen);
1158 let mut entries = walk(dir.path(), 2, Order::Completion, move |entry| {
1159 if entry.depth == 1
1160 && seen_in_worker.fetch_add(1, AtomicOrdering::Relaxed) == ENTRY_CHUNK_SIZE
1161 {
1162 continue_rx
1163 .lock()
1164 .unwrap()
1165 .recv_timeout(std::time::Duration::from_secs(2))
1166 .expect("the first metadata batch should arrive before enumeration finishes");
1167 }
1168 true
1169 });
1170
1171 assert_eq!(
1172 entries.next().unwrap().unwrap().depth,
1173 0,
1174 "the root entry should be yielded first"
1175 );
1176 assert_eq!(
1177 entries.next().unwrap().unwrap().depth,
1178 1,
1179 "the first metadata batch should be yielded before enumeration resumes"
1180 );
1181 continue_tx.send(()).unwrap();
1182 entries.for_each(drop);
1183 assert_eq!(
1184 seen.load(AtomicOrdering::Relaxed),
1185 ENTRY_CHUNK_SIZE + 1,
1186 "all directory entries should be inspected"
1187 );
1188 }
1189}