Skip to content

Commit e1412a9

Browse files
committed
test: deduplicate arguments
1 parent 01e9fbc commit e1412a9

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

tests/_utils.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@ impl SampleWorkspace {
143143
SampleWorkspace(temp)
144144
}
145145

146+
pub fn simple_tree_with_some_symlinks_and_hardlinks(sizes: [usize; 5]) -> Self {
147+
use std::os::unix::fs::symlink;
148+
let workspace = SampleWorkspace::simple_tree_with_some_hardlinks(sizes);
149+
150+
macro_rules! symlink {
151+
($link_name:literal -> $target:literal) => {
152+
let link_name = $link_name;
153+
let target = $target;
154+
if let Err(error) = symlink(target, workspace.join(link_name)) {
155+
panic!("Failed create symbolic link {link_name} pointing to {target}: {error}");
156+
}
157+
};
158+
}
159+
160+
symlink!("workspace-itself" -> ".");
161+
symlink!("main/main-itself" -> ".");
162+
symlink!("main/parent-of-main" -> "..");
163+
symlink!("main-mirror" -> "./main");
164+
symlink!("sources-mirror" -> "./main/sources");
165+
166+
workspace
167+
}
168+
146169
/// Set up a temporary directory for tests.
147170
///
148171
/// This directory would have a single file being hard-linked multiple times.

tests/hardlinks_deduplication_multi_args.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,237 @@ fn multiple_hardlinks_to_a_single_file() {
384384
expected_hardlinks_summary.trim_end(),
385385
);
386386
}
387+
388+
#[test]
389+
fn multiple_duplicated_arguments() {
390+
#![expect(clippy::identity_op)]
391+
392+
let sizes = [200_000, 220_000, 310_000, 110_000, 210_000];
393+
let workspace = SampleWorkspace::simple_tree_with_some_symlinks_and_hardlinks(sizes);
394+
395+
let mut tree = Command::new(PDU)
396+
.with_current_dir(&workspace)
397+
.with_arg("--quantity=apparent-size")
398+
.with_arg("--deduplicate-hardlinks")
399+
.with_arg("--json-output")
400+
.with_arg("main/sources") // expected to be kept
401+
.with_arg("main/main-itself/sources") // expected to be removed
402+
.with_arg("workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks") // expected to be kept
403+
.with_arg("main/internal-hardlinks") // expected to be removed
404+
.pipe(stdio)
405+
.output()
406+
.expect("spawn command")
407+
.pipe(stdout_text)
408+
.pipe_as_ref(serde_json::from_str::<JsonData>)
409+
.expect("parse stdout as JsonData")
410+
.body
411+
.pipe(JsonTree::<Bytes>::try_from)
412+
.expect("get tree of bytes");
413+
sort_reflection_by(&mut tree, |a, b| a.name.cmp(&b.name));
414+
let tree = tree;
415+
416+
let file_size = |name: &str| {
417+
workspace
418+
.join("main/sources")
419+
.join(name)
420+
.pipe_as_ref(read_apparent_size)
421+
.pipe(Bytes::new)
422+
};
423+
424+
let inode_size = |path: &str| {
425+
workspace
426+
.join(path)
427+
.pipe_as_ref(read_apparent_size)
428+
.pipe(Bytes::new)
429+
};
430+
431+
let file_inode = |name: &str| {
432+
workspace
433+
.join("main/sources")
434+
.join(name)
435+
.pipe_as_ref(read_inode_number)
436+
.pipe(InodeNumber::from)
437+
};
438+
439+
let shared_paths = |suffices: &[&str]| {
440+
suffices
441+
.iter()
442+
.map(PathBuf::from)
443+
.collect::<HashSet<_>>()
444+
.pipe(LinkPathListReflection)
445+
};
446+
447+
let actual_size = tree.size;
448+
let expected_size = Bytes::new(0)
449+
+ inode_size("main/sources")
450+
+ inode_size("main/internal-hardlinks")
451+
+ file_size("no-hardlinks.txt")
452+
+ file_size("one-internal-hardlink.txt")
453+
+ file_size("two-internal-hardlinks.txt")
454+
+ file_size("one-external-hardlink.txt")
455+
+ file_size("one-internal-one-external-hardlinks.txt");
456+
assert_eq!(actual_size, expected_size);
457+
458+
let actual_tree = &tree.tree;
459+
let expected_tree = {
460+
let mut tree = Command::new(PDU)
461+
.with_current_dir(&workspace)
462+
.with_arg("--quantity=apparent-size")
463+
.with_arg("--deduplicate-hardlinks")
464+
.with_arg("--json-output")
465+
.with_arg("main")
466+
.pipe(stdio)
467+
.output()
468+
.expect("spawn command")
469+
.pipe(stdout_text)
470+
.pipe_as_ref(serde_json::from_str::<JsonData>)
471+
.expect("parse stdout as JsonData")
472+
.body
473+
.pipe(JsonTree::<Bytes>::try_from)
474+
.expect("get tree of bytes")
475+
.tree;
476+
tree.name = "(total)".to_string();
477+
tree.size = expected_size;
478+
for child in &mut tree.children {
479+
let name = match child.name.as_str() {
480+
"sources" => "main/sources",
481+
"internal-hardlinks" => {
482+
"workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks"
483+
}
484+
name => panic!("Unexpected name: {name:?}"),
485+
};
486+
child.name = name.to_string();
487+
}
488+
sort_reflection_by(&mut tree, |a, b| a.name.cmp(&b.name));
489+
tree
490+
};
491+
assert_eq!(actual_tree, &expected_tree);
492+
493+
let actual_shared_details: Vec<_> = tree
494+
.shared
495+
.details
496+
.as_ref()
497+
.expect("get details")
498+
.iter()
499+
.cloned()
500+
.collect();
501+
let expected_shared_details = [
502+
ReflectionEntry {
503+
ino: file_inode("one-internal-hardlink.txt"),
504+
size: file_size("one-internal-hardlink.txt"),
505+
links: 1 + 1,
506+
paths: shared_paths(&[
507+
"main/sources/one-internal-hardlink.txt",
508+
"workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks/link-0.txt",
509+
]),
510+
},
511+
ReflectionEntry {
512+
ino: file_inode("two-internal-hardlinks.txt"),
513+
size: file_size("two-internal-hardlinks.txt"),
514+
links: 1 + 2,
515+
paths: shared_paths(&[
516+
"main/sources/two-internal-hardlinks.txt",
517+
"workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks/link-1a.txt",
518+
"workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks/link-1b.txt",
519+
]),
520+
},
521+
ReflectionEntry {
522+
ino: file_inode("one-external-hardlink.txt"),
523+
size: file_size("one-external-hardlink.txt"),
524+
links: 1 + 1,
525+
paths: shared_paths(&["main/sources/one-external-hardlink.txt"]),
526+
},
527+
ReflectionEntry {
528+
ino: file_inode("one-internal-one-external-hardlinks.txt"),
529+
size: file_size("one-internal-one-external-hardlinks.txt"),
530+
links: 1 + 1 + 1,
531+
paths: shared_paths(&[
532+
"main/sources/one-internal-one-external-hardlinks.txt",
533+
"workspace-itself/main/parent-of-main/main-mirror/internal-hardlinks/link-3a.txt",
534+
]),
535+
},
536+
]
537+
.into_sorted_by_key(|item| u64::from(item.ino));
538+
assert_eq!(actual_shared_details, expected_shared_details);
539+
540+
let actual_shared_summary = tree.shared.summary;
541+
let expected_shared_summary = Summary::default()
542+
.with_inodes(0 + 1 + 1 + 1 + 1)
543+
.with_exclusive_inodes(0 + 1 + 1 + 0 + 0)
544+
.with_all_links(0 + 2 + 3 + 2 + 3)
545+
.with_detected_links(0 + 2 + 3 + 1 + 2)
546+
.with_exclusive_links(0 + 2 + 3 + 0 + 0)
547+
.with_shared_size(
548+
Bytes::new(0)
549+
+ file_size("one-internal-hardlink.txt")
550+
+ file_size("two-internal-hardlinks.txt")
551+
+ file_size("one-external-hardlink.txt")
552+
+ file_size("one-internal-one-external-hardlinks.txt"),
553+
)
554+
.with_exclusive_shared_size(
555+
Bytes::new(0)
556+
+ file_size("one-internal-hardlink.txt")
557+
+ file_size("two-internal-hardlinks.txt"),
558+
)
559+
.pipe(Some);
560+
assert_eq!(actual_shared_summary, expected_shared_summary);
561+
562+
let visualization = Command::new(PDU)
563+
.with_current_dir(&workspace)
564+
.with_arg("--quantity=apparent-size")
565+
.with_arg("--deduplicate-hardlinks")
566+
.with_arg("main/sources")
567+
.with_arg("main/internal-hardlinks")
568+
.pipe(stdio)
569+
.output()
570+
.expect("spawn command")
571+
.pipe(stdout_text);
572+
eprintln!("STDOUT:\n{visualization}");
573+
let actual_hardlinks_summary = visualization
574+
.lines()
575+
.skip_while(|line| !line.starts_with("Hardlinks detected!"))
576+
.join("\n");
577+
let expected_hardlinks_summary = {
578+
use parallel_disk_usage::size::Size;
579+
use std::fmt::Write;
580+
let mut summary = String::new();
581+
writeln!(
582+
summary,
583+
"Hardlinks detected! Some files have links outside this tree",
584+
)
585+
.unwrap();
586+
writeln!(
587+
summary,
588+
"* Number of shared inodes: {total} total, {exclusive} exclusive",
589+
total = expected_shared_summary.unwrap().inodes,
590+
exclusive = expected_shared_summary.unwrap().exclusive_inodes,
591+
)
592+
.unwrap();
593+
writeln!(
594+
summary,
595+
"* Total number of links: {total} total, {detected} detected, {exclusive} exclusive",
596+
total = expected_shared_summary.unwrap().all_links,
597+
detected = expected_shared_summary.unwrap().detected_links,
598+
exclusive = expected_shared_summary.unwrap().exclusive_links,
599+
)
600+
.unwrap();
601+
writeln!(
602+
summary,
603+
"* Total shared size: {total} total, {exclusive} exclusive",
604+
total = expected_shared_summary
605+
.unwrap()
606+
.shared_size
607+
.display(BytesFormat::MetricUnits),
608+
exclusive = expected_shared_summary
609+
.unwrap()
610+
.exclusive_shared_size
611+
.display(BytesFormat::MetricUnits),
612+
)
613+
.unwrap();
614+
summary
615+
};
616+
assert_eq!(
617+
actual_hardlinks_summary.trim_end(),
618+
expected_hardlinks_summary.trim_end(),
619+
);
620+
}

0 commit comments

Comments
 (0)