1#![deny(missing_docs)]
2use std::{
6 collections::BTreeSet,
7 ffi::OsString,
8 io,
9 path::{Component, Path, PathBuf},
10 sync::Arc,
11};
12
13use fs_err as fs;
14use indexmap::IndexSet;
15use itertools::Itertools;
16
17#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct PackageName(Arc<str>);
21
22impl PackageName {
23 pub fn new(s: impl Into<Arc<str>>) -> Self {
26 Self(s.into())
27 }
28
29 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33}
34
35impl From<&str> for PackageName {
36 fn from(s: &str) -> Self {
37 Self::new(s)
38 }
39}
40
41impl From<String> for PackageName {
42 fn from(s: String) -> Self {
43 Self::new(s)
44 }
45}
46
47impl From<&String> for PackageName {
48 fn from(s: &String) -> Self {
49 Self::new(s.clone())
50 }
51}
52
53impl AsRef<str> for PackageName {
54 fn as_ref(&self) -> &str {
55 &self.0
56 }
57}
58
59impl AsRef<Path> for PackageName {
60 fn as_ref(&self) -> &Path {
61 let s: &str = self.as_ref();
62 Path::new(s)
63 }
64}
65
66impl std::fmt::Display for PackageName {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 write!(f, "{}", self.0)
69 }
70}
71
72impl std::ops::Deref for PackageName {
73 type Target = str;
74
75 fn deref(&self) -> &Self::Target {
76 &self.0
77 }
78}
79
80pub type ToClobbers = Vec<(PathBuf, PackageName)>;
82pub type FromClobbers = Vec<(PathBuf, PackageName)>;
84pub type Changes = (ToClobbers, FromClobbers);
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub struct ClobberedPath {
90 pub winner: PackageName,
92
93 pub losers: Vec<PackageName>,
95}
96
97#[derive(Default, Debug, Clone, PartialEq, Eq)]
99struct PathTrieNode {
100 prefixes: ahash::HashSet<PackageName>,
102 terminals: ahash::HashSet<PackageName>,
104 children: ahash::HashMap<OsString, PathTrieNode>,
106}
107
108#[derive(Default, Debug, Clone, PartialEq, Eq)]
110pub struct PathResolver {
111 root: PathTrieNode,
112 packages: IndexSet<PackageName>,
113}
114
115impl PathResolver {
116 pub fn new() -> Self {
118 Self {
119 root: PathTrieNode::default(),
120 packages: IndexSet::new(),
121 }
122 }
123
124 pub fn packages(&self) -> &IndexSet<PackageName> {
126 &self.packages
127 }
128
129 fn insert_file_owned<P: AsRef<Path>>(&mut self, path: P, package: PackageName) {
131 let path = path.as_ref();
132 assert!(
133 path.is_relative(),
134 "All inserted paths must be relative; got {path:?}"
135 );
136
137 let mut node = &mut self.root;
138 for comp in path.components().map(|c| c.as_os_str().to_os_string()) {
139 node.prefixes.insert(package.clone());
140 node = node.children.entry(comp).or_default();
141 }
142 node.prefixes.insert(package.clone());
143 node.terminals.insert(package);
144 }
145
146 fn get_node_mut<'a>(&'a mut self, path: &Path) -> Option<&'a mut PathTrieNode> {
148 let mut cur = &mut self.root;
149 for comp in path.components().map(Component::as_os_str) {
150 cur = cur.children.get_mut(comp)?;
151 }
152 Some(cur)
153 }
154
155 fn propagate_prefix(node: &mut PathTrieNode, package_name: PackageName) {
157 node.prefixes.insert(package_name.clone());
158 for child in node.children.values_mut() {
159 Self::propagate_prefix(child, package_name.clone());
160 }
161 }
162
163 pub fn insert_package<P: AsRef<Path>>(
171 &mut self,
172 package: PackageName,
173 paths: &[P],
174 ) -> Vec<PathBuf> {
175 self.packages.insert(package.clone());
177
178 let mut conflicts = BTreeSet::default();
179 let mut dir_inserts = Vec::new();
181
182 for p in paths {
205 let p = p.as_ref();
206
207 let mut current_node = &self.root;
208 let mut found_node = None;
209 let mut has_conflict = false;
210
211 let mut components = p.components().peekable();
212
213 while let Some(component) = components.next() {
214 let comp = component.as_os_str();
215 let is_last = components.peek().is_none();
216
217 match current_node.children.get(comp) {
218 Some(node) => {
219 if !is_last && !node.terminals.is_empty() {
221 conflicts.insert(p.to_path_buf());
222 has_conflict = true;
223 break;
224 }
225
226 current_node = node;
227
228 if is_last {
229 found_node = Some(node);
230 }
231 }
232 None => break, }
234 }
235
236 if !has_conflict {
238 if let Some(n) = found_node {
239 if !n.terminals.is_empty() {
241 conflicts.insert(p.to_path_buf());
242 continue;
243 }
244 if !n.children.is_empty() {
246 let pbuf = p.to_path_buf();
247 conflicts.insert(pbuf.clone());
248 dir_inserts.push(pbuf);
249 }
250 }
251 }
252 }
253
254 for p in paths {
256 self.insert_file_owned(p, package.clone());
257 }
258
259 for pbuf in dir_inserts {
261 if let Some(n) = self.get_node_mut(&pbuf) {
262 Self::propagate_prefix(n, package.clone());
263 }
264 }
265
266 conflicts.into_iter().collect()
267 }
268
269 pub fn unregister_package<N: Into<PackageName>>(&mut self, package: N) -> Changes {
274 fn collect_next_candidate_paths(
275 package: PackageName,
276 candidates: &indexmap::set::Slice<PackageName>,
277 n: &PathTrieNode,
278 to_add: &mut Vec<(PathBuf, PackageName)>,
279 path: &mut PathBuf,
280 under_removed: bool,
281 ) {
282 let removed_here = n.terminals.contains(&package);
284 let prefix_here = n.prefixes.contains(&package);
285 let active = under_removed || removed_here || prefix_here;
287 if !active {
288 return;
289 }
290
291 for candidate in candidates {
293 if n.terminals.contains(candidate) {
294 to_add.push((path.clone(), candidate.clone()));
295 break;
296 }
297 }
298
299 let next_under = under_removed || removed_here;
301 for (comp, child) in &n.children {
303 path.push(comp);
304 collect_next_candidate_paths(
305 package.clone(),
306 candidates,
307 child,
308 to_add,
309 path,
310 next_under,
311 );
312 path.pop();
313 }
314 }
315
316 fn prune(n: &mut PathTrieNode) -> bool {
317 n.children.retain(|_, c| !prune(c));
318 n.prefixes.is_empty() && n.terminals.is_empty() && n.children.is_empty()
319 }
320
321 fn rm(n: &mut PathTrieNode, pkg: PackageName) {
322 n.prefixes.remove(&pkg);
323 n.terminals.remove(&pkg);
324 for child in n.children.values_mut() {
325 rm(child, pkg.clone());
326 }
327 }
328
329 let package = package.into();
330
331 if !self.packages.contains(&package) {
332 return Default::default();
333 }
334
335 let mut from_clobbers = vec![];
336 let to_clobbers = vec![];
337
338 if let Some(candidates) = self
339 .packages
340 .get_index_of(&package)
341 .and_then(|idx| self.packages.get_range(idx + 1..))
342 {
343 collect_next_candidate_paths(
344 package.clone(),
345 candidates,
346 &self.root,
347 &mut from_clobbers,
348 &mut PathBuf::new(),
349 false,
350 );
351 }
352
353 self.packages.shift_remove(&package);
354
355 rm(&mut self.root, package);
356
357 self.root.children.retain(|_, c| !prune(c));
358
359 (to_clobbers, from_clobbers)
360 }
361
362 pub fn reprioritize_packages(&mut self, new_order: Vec<PackageName>) -> Changes {
364 fn rank<'a>(
365 order: impl IntoIterator<Item = &'a PackageName>,
366 ) -> ahash::HashMap<&'a PackageName, usize> {
367 order.into_iter().enumerate().map(|(i, v)| (v, i)).collect()
368 }
369
370 fn collect_removed_subtree(
371 n: &PathTrieNode,
372 cur: &mut PathBuf,
373 old_rank: &ahash::HashMap<&PackageName, usize>,
374 to_clobbers: &mut ToClobbers,
375 ) {
376 let old_winner = n
377 .prefixes
378 .iter()
379 .max_by_key(|p| old_rank.get(p).copied().unwrap_or(usize::MAX));
380 if let Some(p) = old_winner {
381 if n.terminals.contains(p) {
382 to_clobbers.push((cur.clone(), p.clone()));
383 }
384 }
385 for (comp, child) in &n.children {
386 cur.push(comp);
387 collect_removed_subtree(child, cur, old_rank, to_clobbers);
388 cur.pop();
389 }
390 }
391
392 fn collect_new_winners(
393 n: &PathTrieNode,
394 cur: &mut PathBuf,
395 new_rank: &ahash::HashMap<&PackageName, usize>,
396 from_clobbers: &mut FromClobbers,
397 ) {
398 let new_winner = n
399 .prefixes
400 .iter()
401 .max_by_key(|p| new_rank.get(p).copied().unwrap_or(usize::MAX));
402 if let Some(p) = new_winner {
403 if n.terminals.contains(p) {
404 from_clobbers.push((cur.clone(), p.clone()));
405 return; }
407 for (comp, child) in &n.children {
408 cur.push(comp);
409 collect_new_winners(child, cur, new_rank, from_clobbers);
410 cur.pop();
411 }
412 }
413 }
414
415 fn dfs(
416 n: &PathTrieNode,
417 cur: &mut PathBuf,
418 old_rank: &ahash::HashMap<&PackageName, usize>,
419 new_rank: &ahash::HashMap<&PackageName, usize>,
420 to_clobbers: &mut ToClobbers,
421 from_clobbers: &mut FromClobbers,
422 ) {
423 let old_winner = n
424 .prefixes
425 .iter()
426 .max_by_key(|p| old_rank.get(p).copied().unwrap_or(usize::MAX));
427 let new_winner = n
428 .prefixes
429 .iter()
430 .max_by_key(|p| new_rank.get(p).copied().unwrap_or(usize::MAX));
431
432 let old_is_file = old_winner.is_some_and(|p| n.terminals.contains(p));
433 let new_is_file = new_winner.is_some_and(|p| n.terminals.contains(p));
434
435 match (old_is_file, new_is_file) {
436 (true, true) => {
437 let old = old_winner.unwrap();
438 let new = new_winner.unwrap();
439 if old != new {
440 to_clobbers.push((cur.clone(), old.clone()));
441 from_clobbers.push((cur.clone(), new.clone()));
442 }
443 }
445 (false, true) => {
446 let new = new_winner.unwrap();
447 from_clobbers.push((cur.clone(), new.clone()));
448 collect_removed_subtree(n, cur, old_rank, to_clobbers);
449 }
451 (true, false) => {
452 let old = old_winner.unwrap();
453 to_clobbers.push((cur.clone(), old.clone()));
454 collect_new_winners(n, cur, new_rank, from_clobbers);
455 }
457 (false, false) => {
458 for (comp, child) in &n.children {
460 cur.push(comp);
461 dfs(child, cur, old_rank, new_rank, to_clobbers, from_clobbers);
462 cur.pop();
463 }
464 }
465 }
466 }
467
468 {
469 let is_reordering = 'reorder: {
470 if self.packages.len() != new_order.len() {
471 break 'reorder false;
472 }
473 let self_pkg_set: ahash::HashSet<&PackageName> = self.packages.iter().collect();
474 let new_pkg_set: ahash::HashSet<&PackageName> = new_order.iter().collect();
475 self_pkg_set == new_pkg_set
476 };
477
478 assert!(
479 is_reordering,
480 "Expected just reordering, got something else.
481Old:
482{:#?}
483New:
484{:#?}
485",
486 self.packages
487 .iter()
488 .cloned()
489 .sorted()
490 .collect::<Vec<PackageName>>(),
491 new_order
492 .iter()
493 .cloned()
494 .sorted()
495 .collect::<Vec<PackageName>>()
496 );
497 }
498
499 let old_rank = rank(self.packages.iter().rev());
501 let new_rank = rank(new_order.iter());
502
503 let mut to_clobbers = Vec::new();
504 let mut from_clobbers = Vec::new();
505
506 let mut buf = PathBuf::new();
507 dfs(
508 &self.root,
509 &mut buf,
510 &old_rank,
511 &new_rank,
512 &mut to_clobbers,
513 &mut from_clobbers,
514 );
515
516 self.packages.clear();
517 self.packages.extend(new_order.into_iter().rev());
518
519 (to_clobbers, from_clobbers)
520 }
521
522 pub fn sync_clobbers(
527 target_prefix: &Path,
528 clobbers_dir: &Path,
529 to_clobbers: &[(PathBuf, PackageName)],
530 from_clobbers: &[(PathBuf, PackageName)],
531 ) -> io::Result<()> {
532 fn mv(src: PathBuf, dst: PathBuf) -> io::Result<()> {
533 tracing::trace!("Moving from {} to {}", src.display(), dst.display());
534 if let Some(p) = dst.parent() {
535 fs::create_dir_all(p)?;
536 }
537 fs::rename(src, dst)
538 }
539
540 fn check_file_exists(path: &Path) -> bool {
541 path.symlink_metadata().is_ok()
542 }
543
544 for (p, pkg) in to_clobbers {
545 let src = target_prefix.join(p);
546 let dst = clobbers_dir.join::<&Path>(pkg.as_ref()).join(p);
547 if check_file_exists(&src) && !check_file_exists(&dst) {
548 mv(src, dst)?;
549 }
550 }
551
552 for (p, pkg) in from_clobbers {
553 let src = clobbers_dir.join::<&Path>(pkg.as_ref()).join(p);
554 let dst = target_prefix.join(p);
555 if check_file_exists(&src) && !check_file_exists(&dst) {
556 mv(src, dst)?;
557 }
558 }
559
560 Ok(())
561 }
562
563 pub fn packages_for_prefix<P: AsRef<Path>>(
565 &self,
566 path: P,
567 ) -> Option<&ahash::HashSet<PackageName>> {
568 let mut cur = &self.root;
569
570 for comp in path.as_ref().components().map(Component::as_os_str) {
571 cur = cur.children.get(comp)?;
572 }
573
574 Some(&cur.prefixes)
575 }
576
577 pub fn packages_for_exact<P: AsRef<Path>>(
579 &self,
580 path: P,
581 ) -> Option<&ahash::HashSet<PackageName>> {
582 Self::get_node(&self.root, path.as_ref()).map(|n| &n.terminals)
583 }
584
585 pub fn find_conflicts(&self) -> Vec<(PathBuf, Vec<PackageName>)> {
587 fn dfs(n: &PathTrieNode, cur: &mut PathBuf, out: &mut Vec<(PathBuf, Vec<PackageName>)>) {
588 if n.terminals.len() > 1 {
589 out.push((cur.clone(), n.terminals.iter().cloned().collect()));
590 }
591 for (c, child) in &n.children {
592 cur.push(c);
593 dfs(child, cur, out);
594 cur.pop();
595 }
596 }
597
598 let mut out = Vec::new();
599 let mut buf = PathBuf::new();
600 dfs(&self.root, &mut buf, &mut out);
601 out
602 }
603
604 fn get_node<'a>(root: &'a PathTrieNode, path: &Path) -> Option<&'a PathTrieNode> {
606 let mut cur = root;
607 for comp in path.components().map(Component::as_os_str) {
608 cur = cur.children.get(comp)?;
609 }
610 Some(cur)
611 }
612
613 pub fn collect_clobbered_paths(&self) -> ahash::HashMap<PathBuf, ClobberedPath> {
616 fn dfs(
617 node: &PathTrieNode,
618 path: &mut PathBuf,
619 packages: &IndexSet<PackageName>,
620 results: &mut ahash::HashMap<PathBuf, ClobberedPath>,
621 ) {
622 if !node.terminals.is_empty() {
623 if let Some(winner) = packages.iter().find(|pkg| node.terminals.contains(pkg)) {
625 let others: Vec<PackageName> = node
627 .terminals
628 .iter()
629 .filter(|&p| (p.as_ref() as &Path) != (winner.as_ref() as &Path))
630 .cloned()
631 .collect();
632 if !others.is_empty() {
633 results.insert(
634 path.clone(),
635 ClobberedPath {
636 winner: winner.clone(),
637 losers: others,
638 },
639 );
640 }
641 }
642 }
643 for (comp, child) in &node.children {
644 path.push(comp);
645 dfs(child, path, packages, results);
646 path.pop();
647 }
648 }
649
650 let mut results = ahash::HashMap::default();
651 dfs(
652 &self.root,
653 &mut PathBuf::new(),
654 &self.packages,
655 &mut results,
656 );
657 results
658 }
659}
660
661#[cfg(test)]
663mod tests {
664 use super::*;
665 use std::{
666 fs,
667 fs::File,
668 io::{Read, Write},
669 path::PathBuf,
670 };
671 use tempfile::TempDir;
672
673 #[test]
674 fn test_insert_file_vs_file_conflict() {
675 let mut resolver = PathResolver::new();
676 assert!(resolver
677 .insert_package("pkg1".into(), &["foo.txt"])
678 .is_empty());
679 let conflicts = resolver.insert_package("pkg2".into(), &["foo.txt"]);
680 assert_eq!(conflicts, vec![PathBuf::from("foo.txt")]);
681 }
682
683 #[test]
684 fn test_insert_nested_file_vs_file_conflict() {
685 let mut resolver = PathResolver::new();
686 assert!(resolver
687 .insert_package("pkg1".into(), &["foo/bar.txt"])
688 .is_empty());
689 let conflicts = resolver.insert_package("pkg2".into(), &["foo/bar.txt"]);
690 assert_eq!(conflicts, vec![PathBuf::from("foo/bar.txt")]);
691 }
692
693 #[test]
694 fn test_insert_dir_vs_file_conflict() {
695 let mut resolver = PathResolver::new();
696 assert!(resolver
697 .insert_package("pkg1".into(), &["foo/bar.txt", "foo/baz.txt"])
698 .is_empty());
699 let mut conflicts = resolver.insert_package("pkg2".into(), &["foo"]);
700 conflicts.sort();
701 assert_eq!(conflicts, vec![PathBuf::from("foo")]);
702 }
703
704 #[test]
705 fn test_insert_file_vs_dir_conflict() {
706 let mut resolver = PathResolver::new();
707 assert!(resolver.insert_package("pkg1".into(), &["foo"]).is_empty());
708 let conflicts = resolver.insert_package("pkg2".into(), &["foo/bar.txt"]);
709 assert_eq!(conflicts, vec![PathBuf::from("foo/bar.txt")]);
710 }
711
712 #[test]
713 fn test_no_conflict_on_sibling() {
714 let mut resolver = PathResolver::new();
715 assert!(resolver.insert_package("a".into(), &["a/x"]).is_empty());
716 assert!(resolver.insert_package("b".into(), &["b/y"]).is_empty());
717 }
718
719 #[test]
720 fn test_no_conflict_on_dir_sibling() {
721 let mut resolver = PathResolver::new();
722 assert!(resolver.insert_package("a".into(), &["a/x"]).is_empty());
723 assert!(resolver.insert_package("b".into(), &["a/y"]).is_empty());
724 }
725
726 #[test]
727 fn test_unregister_package() {
728 let mut resolver = PathResolver::new();
729 let paths = ["foo.txt", "foo/bar.txt"];
730 assert!(resolver.insert_package("pkg".into(), &paths).is_empty());
731 resolver.unregister_package("pkg");
732 assert!(resolver
733 .packages_for_exact("foo.txt")
734 .is_none_or(ahash::HashSet::is_empty));
735 assert!(resolver.packages_for_prefix("foo").is_none());
736 }
737
738 #[test]
739 fn test_reprioritize_noop() {
740 let mut resolver = PathResolver::new();
741 resolver.insert_package("pkg1".into(), &["a.txt"]);
742 resolver.insert_package("pkg2".into(), &["b.txt"]);
743 let (removed, added) = resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
744 assert!(removed.is_empty());
745 assert!(added.is_empty());
746 }
747
748 #[test]
749 fn test_reprioritize_file_vs_file() {
750 let mut resolver = PathResolver::new();
751 resolver.insert_package("pkg1".into(), &["foo.txt"]);
752 resolver.insert_package("pkg2".into(), &["foo.txt"]);
753 let (removed, added) = resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
754 assert_eq!(removed, vec![(PathBuf::from("foo.txt"), "pkg1".into())]);
755 assert_eq!(added, vec![(PathBuf::from("foo.txt"), "pkg2".into())]);
756 }
757
758 #[test]
759 fn test_reprioritize_file_vs_dir() {
760 let mut resolver = PathResolver::new();
761 resolver.insert_package("pkg1".into(), &["foo"]);
762 resolver.insert_package("pkg2".into(), &["foo/bar.txt"]);
763 let (removed, added) = resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
764 assert_eq!(removed, vec![(PathBuf::from("foo"), "pkg1".into())]);
765 assert_eq!(added, vec![(PathBuf::from("foo/bar.txt"), "pkg2".into())]);
766 }
767
768 #[test]
769 fn test_reprioritize_dir_vs_file() {
770 let mut resolver = PathResolver::new();
771 resolver.insert_package("pkg1".into(), &["foo/bar.txt"]);
772 resolver.insert_package("pkg2".into(), &["foo"]);
773 let (removed, added) = resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
774 assert_eq!(removed, vec![(PathBuf::from("foo/bar.txt"), "pkg1".into())]);
775 assert_eq!(added, vec![(PathBuf::from("foo"), "pkg2".into())]);
776 }
777
778 #[test]
779 fn test_reprioritize_dir_vs_dir() {
780 let mut resolver = PathResolver::new();
781 resolver.insert_package("pkg1".into(), &["foo/bar1.txt", "foo/bar2.txt"]);
782 let mut conflict = resolver.insert_package("pkg2".into(), &["foo/bar2.txt"]);
783 conflict.sort();
784 assert_eq!(conflict, vec![PathBuf::from("foo/bar2.txt")]);
785 let (removed, added) = resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
786 assert_eq!(
788 removed,
789 vec![(PathBuf::from("foo/bar2.txt"), "pkg1".into())],
790 );
791 assert_eq!(added, vec![(PathBuf::from("foo/bar2.txt"), "pkg2".into())]);
792 }
793
794 #[test]
795 fn test_reprioritize_file_vs_dir_vs_dir_with_permuted_insertion_order() {
796 let priority_order = vec!["pkg1".into(), "pkg2".into(), "pkg3".into()];
797
798 let pkgs: &[(PackageName, &[&str])] = &[
800 ("pkg1".into(), &["foo"]),
801 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
802 ("pkg3".into(), &["foo/bar2.txt"]),
803 ];
804
805 let mut resolver = PathResolver::new();
806 for (pkg_name, paths) in pkgs {
807 resolver.insert_package(pkg_name.clone(), paths);
808 }
809
810 let (removed, mut added) = resolver.reprioritize_packages(priority_order.clone());
811 assert_eq!(removed, vec![(PathBuf::from("foo"), "pkg1".into())],);
812 added.sort();
813 assert_eq!(
814 added,
815 vec![
816 (PathBuf::from("foo/bar1.txt"), "pkg2".into()),
817 (PathBuf::from("foo/bar2.txt"), "pkg3".into())
818 ]
819 );
820
821 let pkgs: &[(String, &[&str])] = &[
823 ("pkg1".into(), &["foo"]),
824 ("pkg3".into(), &["foo/bar2.txt"]),
825 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
826 ];
827
828 let mut resolver = PathResolver::new();
829 for (pkg_name, paths) in pkgs {
830 resolver.insert_package(pkg_name.into(), paths);
831 }
832
833 let (removed, mut added) = resolver.reprioritize_packages(priority_order.clone());
834 assert_eq!(removed, vec![(PathBuf::from("foo"), "pkg1".into())],);
835 added.sort();
836 assert_eq!(
837 added,
838 vec![
839 (PathBuf::from("foo/bar1.txt"), "pkg2".into()),
840 (PathBuf::from("foo/bar2.txt"), "pkg3".into())
841 ]
842 );
843
844 let pkgs: &[(String, &[&str])] = &[
846 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
847 ("pkg1".into(), &["foo"]),
848 ("pkg3".into(), &["foo/bar2.txt"]),
849 ];
850
851 let mut resolver = PathResolver::new();
852 for (pkg_name, paths) in pkgs {
853 resolver.insert_package(pkg_name.into(), paths);
854 }
855
856 let (removed, added) = resolver.reprioritize_packages(priority_order.clone());
857 assert_eq!(
858 removed,
859 vec![(PathBuf::from("foo/bar2.txt"), "pkg2".into())],
860 );
861 assert_eq!(added, vec![(PathBuf::from("foo/bar2.txt"), "pkg3".into())]);
862
863 let pkgs: &[(String, &[&str])] = &[
865 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
866 ("pkg3".into(), &["foo/bar2.txt"]),
867 ("pkg1".into(), &["foo"]),
868 ];
869
870 let mut resolver = PathResolver::new();
871 for (pkg_name, paths) in pkgs {
872 resolver.insert_package(pkg_name.into(), paths);
873 }
874
875 let (removed, added) = resolver.reprioritize_packages(priority_order.clone());
876 assert_eq!(
877 removed,
878 vec![(PathBuf::from("foo/bar2.txt"), "pkg2".into())],
879 );
880 assert_eq!(added, vec![(PathBuf::from("foo/bar2.txt"), "pkg3".into())]);
881
882 let pkgs: &[(String, &[&str])] = &[
884 ("pkg3".into(), &["foo/bar2.txt"]),
885 ("pkg1".into(), &["foo"]),
886 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
887 ];
888
889 let mut resolver = PathResolver::new();
890 for (pkg_name, paths) in pkgs {
891 resolver.insert_package(pkg_name.into(), paths);
892 }
893
894 let (removed, added) = resolver.reprioritize_packages(priority_order.clone());
895 assert!(removed.is_empty());
896 assert!(added.is_empty());
897
898 let pkgs: &[(String, &[&str])] = &[
900 ("pkg3".into(), &["foo/bar2.txt"]),
901 ("pkg2".into(), &["foo/bar1.txt", "foo/bar2.txt"]),
902 ("pkg1".into(), &["foo"]),
903 ];
904
905 let mut resolver = PathResolver::new();
906 for (pkg_name, paths) in pkgs {
907 resolver.insert_package(pkg_name.into(), paths);
908 }
909
910 let (removed, added) = resolver.reprioritize_packages(priority_order.clone());
911 assert!(removed.is_empty());
912 assert!(added.is_empty());
913 }
914
915 #[test]
916 fn test_tags_queries() {
917 let mut resolver = PathResolver::new();
918 resolver.insert_package("pkg".into(), &["d1/f1.txt", "d1/f2.txt", "d2/f3.txt"]);
919 let p1 = resolver.packages_for_prefix("d1").unwrap();
920 assert_eq!(p1.len(), 1);
921 assert!(p1.contains(&"pkg".into()));
922 let e = resolver.packages_for_exact("d1/f2.txt").unwrap();
923 assert_eq!(e.len(), 1);
924 assert!(e.contains(&"pkg".into()));
925 }
926
927 #[test]
928 fn test_sync_clobbers_file_vs_file() {
929 let tmp = TempDir::new().unwrap();
930 let target_prefix = tmp.path();
931 let clobbers = tmp.path().join("__clobbers__");
932 fs::create_dir_all(target_prefix).unwrap();
933 fs::create_dir_all(clobbers.join("pkg2")).unwrap();
934
935 File::create(target_prefix.join("foo.txt"))
936 .unwrap()
937 .write_all(b"pkg1")
938 .unwrap();
939 File::create(clobbers.join("pkg2").join("foo.txt"))
940 .unwrap()
941 .write_all(b"pkg2")
942 .unwrap();
943
944 let mut resolver = PathResolver::new();
945 resolver.insert_package("pkg1".into(), &["foo.txt"]);
946 resolver.insert_package("pkg2".into(), &["foo.txt"]);
947 let (to_clobbers, from_clobbers) =
948 resolver.reprioritize_packages(vec!["pkg1".into(), "pkg2".into()]);
949
950 PathResolver::sync_clobbers(target_prefix, &clobbers, &to_clobbers, &from_clobbers)
951 .unwrap();
952
953 let mut buf = String::new();
954 File::open(target_prefix.join("foo.txt"))
955 .unwrap()
956 .read_to_string(&mut buf)
957 .unwrap();
958 assert_eq!(buf, "pkg2");
959
960 let mut buf = String::new();
961 File::open(clobbers.join("pkg1").join("foo.txt"))
962 .unwrap()
963 .read_to_string(&mut buf)
964 .unwrap();
965 assert_eq!(buf, "pkg1");
966 }
967
968 #[test]
970 fn test_insert_file_vs_dir_conflict_unregister() {
971 let mut resolver = PathResolver::new();
972 assert!(resolver.insert_package("pkg1".into(), &["foo"]).is_empty());
973 resolver.insert_package("pkg2".into(), &["foo/bar.txt"]);
974 let moves = resolver.unregister_package("pkg1");
975 assert_eq!(
976 moves,
977 (vec![], vec![(PathBuf::from("foo/bar.txt"), "pkg2".into())])
978 );
979 }
980
981 #[test]
982 fn test_collect_clobbered_no_conflict() {
983 let mut resolver = PathResolver::new();
984 assert!(resolver
985 .insert_package("pkg1".into(), &["a.txt"])
986 .is_empty());
987
988 let clobbered = resolver.collect_clobbered_paths();
989 assert!(clobbered.is_empty());
990 }
991
992 #[test]
993 fn test_collect_clobbered_simple_conflict() {
994 let mut resolver = PathResolver::new();
995 assert!(resolver
996 .insert_package("pkg1".into(), &["file.txt"])
997 .is_empty());
998 let conflicts = resolver.insert_package("pkg2".into(), &["file.txt"]);
999 assert_eq!(conflicts, vec![PathBuf::from("file.txt")]);
1000
1001 let clobbered = resolver.collect_clobbered_paths();
1002 assert_eq!(clobbered.len(), 1);
1003 let path = PathBuf::from("file.txt");
1004 let entry = clobbered.get(&path).expect("file.txt should be present");
1005 assert_eq!(entry.winner, "pkg1".into());
1006 assert_eq!(entry.losers, vec!["pkg2".into()]);
1007 }
1008
1009 #[test]
1010 fn test_collect_clobbered_multiple_conflicts() {
1011 let mut resolver = PathResolver::new();
1012 assert!(resolver
1013 .insert_package("pkg1".into(), &["dup.txt"])
1014 .is_empty());
1015 let conflicts = resolver.insert_package("pkg2".into(), &["dup.txt"]);
1016 assert_eq!(conflicts, vec![PathBuf::from("dup.txt")]);
1017 let conflicts = resolver.insert_package("pkg3".into(), &["dup.txt"]);
1018 assert_eq!(conflicts, vec![PathBuf::from("dup.txt")]);
1019
1020 let clobbered = resolver.collect_clobbered_paths();
1021 assert_eq!(clobbered.len(), 1);
1022 let path = PathBuf::from("dup.txt");
1023 let entry = clobbered.get(&path).expect("dup.txt should be present");
1024 assert_eq!(entry.winner, "pkg1".into());
1025 let mut others = entry.losers.clone();
1026 others.sort();
1027 assert_eq!(others, vec!["pkg2".into(), "pkg3".into()]);
1028 }
1029
1030 #[test]
1031 fn test_collect_clobbered_multiple_files() {
1032 let mut resolver = PathResolver::new();
1033 assert!(resolver
1034 .insert_package("pkg1".into(), &["a.txt", "b.txt"])
1035 .is_empty());
1036 let conflicts = resolver.insert_package("pkg2".into(), &["a.txt"]);
1037 assert_eq!(conflicts, vec![PathBuf::from("a.txt")]);
1038
1039 let clobbered = resolver.collect_clobbered_paths();
1040 assert_eq!(clobbered.len(), 1);
1041 let path = PathBuf::from("a.txt");
1042 let entry = clobbered.get(&path).expect("a.txt should be clobbered");
1043 assert_eq!(
1044 entry,
1045 &ClobberedPath {
1046 winner: "pkg1".into(),
1047 losers: vec!["pkg2".into()]
1048 }
1049 );
1050 }
1051
1052 #[test]
1053 fn test_collect_clobbered_directory_conflict() {
1054 let mut resolver = PathResolver::new();
1055 assert!(resolver.insert_package("pkg1".into(), &["dir"]).is_empty());
1056 let conflicts = resolver.insert_package("pkg2".into(), &["dir"]);
1057 assert_eq!(conflicts, vec![PathBuf::from("dir")]);
1058
1059 let clobbered = resolver.collect_clobbered_paths();
1060 assert_eq!(clobbered.len(), 1);
1061 let path = PathBuf::from("dir");
1062 let entry = clobbered.get(&path).expect("dir should be clobbered");
1063 assert_eq!(
1064 entry,
1065 &ClobberedPath {
1066 winner: "pkg1".into(),
1067 losers: vec!["pkg2".into()]
1068 }
1069 );
1070 }
1071}
1072
1073#[cfg(test)]
1074mod props {
1075 use super::*;
1076
1077 use std::collections::BTreeMap;
1078 use std::path::PathBuf;
1079
1080 use proptest::prelude::*;
1081 use proptest::sample::subsequence;
1082 use proptest::string::string_regex;
1083
1084 #[derive(Clone, Debug)]
1086 enum Node {
1087 File,
1088 Dir(BTreeMap<String, Node>),
1089 }
1090
1091 fn collect_paths(node: &Node, cur: &Path, out: &mut Vec<PathBuf>) {
1092 match node {
1093 Node::File => out.push(cur.to_path_buf()),
1094 Node::Dir(children) => {
1095 for (seg, child) in children {
1096 let mut next = cur.to_path_buf();
1097 next.push(seg);
1098 collect_paths(child, &next, out);
1099 }
1100 }
1101 }
1102 }
1103
1104 fn path_trie() -> impl Strategy<Value = Node> {
1107 let leaf = Just(Node::File).boxed();
1109 let dir = |inner: BoxedStrategy<Node>| {
1111 prop::collection::btree_map(
1112 string_regex("[a-z]{1,1}").unwrap(),
1114 inner,
1115 1..=5,
1116 )
1117 .prop_map(Node::Dir)
1118 .boxed()
1119 };
1120
1121 leaf.prop_recursive(5, 64, 5, dir)
1122 }
1123
1124 fn arb_package_set() -> impl Strategy<Value = Vec<(String, Vec<PathBuf>)>> {
1126 let names = subsequence(&["pkg1", "pkg2", "pkg3", "pkg4", "pkg5", "pkg6"], 1..=5)
1128 .prop_map(|v| v.into_iter().map(str::to_string).collect::<Vec<_>>());
1129
1130 names.prop_flat_map(move |pkgs| {
1131 let tries = prop::collection::vec(path_trie(), pkgs.len());
1133
1134 (Just(pkgs.clone()), tries).prop_map(move |(pkgs, trees)| {
1135 pkgs.into_iter()
1136 .zip(trees)
1137 .map(|(pkg, tree)| {
1138 let mut paths = Vec::new();
1139 collect_paths(&tree, &PathBuf::new(), &mut paths);
1140 (pkg, paths)
1141 })
1142 .collect()
1143 })
1144 })
1145 }
1146
1147 fn arb_resolver() -> impl Strategy<Value = (PathResolver, Vec<PackageName>)> {
1150 arb_package_set().prop_map(|pkg_set| {
1151 let mut resolver = PathResolver::new();
1152 let mut initial_order = Vec::with_capacity(pkg_set.len());
1154
1155 for (package, paths) in pkg_set {
1156 let pkg: PackageName = package.into();
1158 let _ = resolver.insert_package(pkg.clone(), &paths);
1159 initial_order.push(pkg);
1160 }
1161
1162 (resolver, initial_order)
1163 })
1164 }
1165
1166 proptest! {
1167 #[test]
1168 fn identity_no_changes((mut resolver, packages) in arb_resolver()) {
1169 let (removed, added) = resolver.reprioritize_packages(packages.into_iter().rev().collect());
1170 prop_assert!(removed.is_empty());
1171 prop_assert!(added.is_empty());
1172 }
1173
1174 #[test]
1175 fn reprioritize_updates_order((mut resolver, packages) in arb_resolver()) {
1176 let new_order: Vec<_> = packages.iter().rev().cloned().collect();
1177 let (_removed, _added) = resolver.reprioritize_packages(new_order.clone());
1178 let current_order: Vec<_> = resolver.packages.iter().rev().cloned().collect();
1179 prop_assert_eq!(current_order, new_order);
1180 }
1181
1182 #[test]
1183 fn idempotent_after_reprioritize((mut resolver, packages) in arb_resolver()) {
1184 let new_order: Vec<_> = packages.clone();
1185 let _first = resolver.reprioritize_packages(new_order.clone());
1186 let (removed2, added2) = resolver.reprioritize_packages(new_order);
1187 prop_assert!(removed2.is_empty());
1188 prop_assert!(added2.is_empty());
1189 }
1190 }
1191}