@@ -20,6 +20,7 @@ use parallel_disk_usage::{
2020} ;
2121use pipe_trait:: Pipe ;
2222use pretty_assertions:: assert_eq;
23+ use rayon:: prelude:: * ;
2324use std:: {
2425 collections:: HashSet ,
2526 iter,
@@ -546,3 +547,131 @@ fn hardlinks_and_non_hardlinks() {
546547 expected_hardlinks_summary. trim_end( ) ,
547548 ) ;
548549}
550+
551+ #[ test]
552+ fn exclusive_hardlinks_only ( ) {
553+ let files_per_branch = 2 * 4 ;
554+ let workspace =
555+ SampleWorkspace :: complex_tree_with_shared_and_unique_files ( files_per_branch, 100_000 ) ;
556+
557+ let tree = Command :: new ( PDU )
558+ . with_current_dir ( & workspace)
559+ . with_arg ( "--min-ratio=0" )
560+ . with_arg ( "--quantity=apparent-size" )
561+ . with_arg ( "--json-output" )
562+ . with_arg ( "--deduplicate-hardlinks" )
563+ . with_arg ( "only-hardlinks/exclusive" )
564+ . pipe ( stdio)
565+ . output ( )
566+ . expect ( "spawn command" )
567+ . pipe ( stdout_text)
568+ . pipe_as_ref ( serde_json:: from_str :: < JsonData > )
569+ . expect ( "parse stdout as JsonData" )
570+ . body
571+ . pipe ( JsonTree :: < Bytes > :: try_from)
572+ . expect ( "get tree of bytes" ) ;
573+
574+ let file_size = workspace
575+ . join ( "only-hardlinks/exclusive/file-0.txt" )
576+ . pipe_as_ref ( read_apparent_size)
577+ . pipe ( Bytes :: new) ;
578+
579+ let inode_size = |path : & str | {
580+ workspace
581+ . join ( path)
582+ . pipe_as_ref ( read_apparent_size)
583+ . pipe ( Bytes :: new)
584+ } ;
585+
586+ let file_inode = |name : & str | {
587+ workspace
588+ . join ( "only-hardlinks/exclusive" )
589+ . join ( name)
590+ . pipe_as_ref ( read_inode_number)
591+ . pipe ( InodeNumber :: from)
592+ } ;
593+
594+ let shared_paths = |file_names : & [ & str ] | {
595+ file_names
596+ . iter ( )
597+ . map ( |file_name| PathBuf :: from ( "only-hardlinks/exclusive" ) . join ( file_name) )
598+ . collect :: < HashSet < _ > > ( )
599+ . pipe ( LinkPathListReflection )
600+ } ;
601+
602+ let actual_size = tree. size ;
603+ let expected_size = inode_size ( "only-hardlinks/exclusive" ) + file_size * files_per_branch;
604+ assert_eq ! ( actual_size, expected_size) ;
605+
606+ let actual_shared_details: Vec < _ > = tree
607+ . shared
608+ . details
609+ . as_ref ( )
610+ . unwrap ( )
611+ . iter ( )
612+ . cloned ( )
613+ . collect ( ) ;
614+ let expected_shared_details = ( 0 ..files_per_branch)
615+ . par_bridge ( )
616+ . map ( |index| ReflectionEntry {
617+ ino : file_inode ( & format ! ( "file-{index}.txt" ) ) ,
618+ size : file_size,
619+ links : 2 ,
620+ paths : shared_paths ( & [ & format ! ( "file-{index}.txt" ) , & format ! ( "link-{index}.txt" ) ] ) ,
621+ } )
622+ . collect :: < Vec < _ > > ( )
623+ . into_sorted_by_key ( |item : & ReflectionEntry < Bytes > | u64:: from ( item. ino ) ) ;
624+ assert_eq ! ( actual_shared_details, expected_shared_details) ;
625+
626+ let actual_shared_summary = tree. shared . summary ;
627+ let expected_shared_summary = Summary :: default ( )
628+ . with_inodes ( files_per_branch)
629+ . with_exclusive_inodes ( files_per_branch)
630+ . with_all_links ( 2 * files_per_branch as u64 )
631+ . with_detected_links ( 2 * files_per_branch)
632+ . with_exclusive_links ( 2 * files_per_branch)
633+ . with_shared_size ( files_per_branch * file_size)
634+ . with_exclusive_shared_size ( files_per_branch * file_size)
635+ . pipe ( Some ) ;
636+ assert_eq ! ( actual_shared_summary, expected_shared_summary) ;
637+
638+ let visualization = Command :: new ( PDU )
639+ . with_current_dir ( & workspace)
640+ . with_arg ( "--quantity=apparent-size" )
641+ . with_arg ( "--deduplicate-hardlinks" )
642+ . with_arg ( "only-hardlinks/exclusive" )
643+ . pipe ( stdio)
644+ . output ( )
645+ . expect ( "spawn command" )
646+ . pipe ( stdout_text) ;
647+
648+ eprintln ! ( "STDOUT:\n {visualization}" ) ;
649+
650+ let actual_hardlinks_summary = visualization
651+ . lines ( )
652+ . skip_while ( |line| !line. starts_with ( "Hardlinks detected!" ) )
653+ . join ( "\n " ) ;
654+ let expected_hardlinks_summary = {
655+ use parallel_disk_usage:: size:: Size ;
656+ use std:: fmt:: Write ;
657+ let mut summary = String :: new ( ) ;
658+ writeln ! (
659+ summary,
660+ "Hardlinks detected! No files have links outside this tree" ,
661+ )
662+ . unwrap ( ) ;
663+ writeln ! ( summary, "* Number of shared inodes: {files_per_branch}" ) . unwrap ( ) ;
664+ writeln ! ( summary, "* Total number of links: {}" , 2 * files_per_branch) . unwrap ( ) ;
665+ writeln ! (
666+ summary,
667+ "* Total shared size: {}" ,
668+ ( file_size * files_per_branch) . display( BytesFormat :: MetricUnits ) ,
669+ )
670+ . unwrap ( ) ;
671+ summary
672+ } ;
673+ assert_eq ! (
674+ actual_hardlinks_summary. trim_end( ) ,
675+ expected_hardlinks_summary. trim_end( ) ,
676+ ) ;
677+ }
0 commit comments