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