@@ -840,3 +840,140 @@ fn exclusive_only_and_external_only_hardlinks() {
840840 expected_hardlinks_summary. trim_end( ) ,
841841 ) ;
842842}
843+
844+ #[ test]
845+ fn external_hardlinks_only ( ) {
846+ let files_per_branch = 2 * 4 ;
847+ let workspace =
848+ SampleWorkspace :: complex_tree_with_shared_and_unique_files ( files_per_branch, 100_000 ) ;
849+
850+ let tree = Command :: new ( PDU )
851+ . with_current_dir ( & workspace)
852+ . with_arg ( "--min-ratio=0" )
853+ . with_arg ( "--quantity=apparent-size" )
854+ . with_arg ( "--json-output" )
855+ . with_arg ( "--deduplicate-hardlinks" )
856+ . with_arg ( "only-hardlinks/external" )
857+ . pipe ( stdio)
858+ . output ( )
859+ . expect ( "spawn command" )
860+ . pipe ( stdout_text)
861+ . pipe_as_ref ( serde_json:: from_str :: < JsonData > )
862+ . expect ( "parse stdout as JsonData" )
863+ . body
864+ . pipe ( JsonTree :: < Bytes > :: try_from)
865+ . expect ( "get tree of bytes" ) ;
866+
867+ let file_size = workspace
868+ . join ( "only-hardlinks/external/linkX-0.txt" )
869+ . pipe_as_ref ( read_apparent_size)
870+ . pipe ( Bytes :: new) ;
871+
872+ let inode_size = |path : & str | {
873+ workspace
874+ . join ( path)
875+ . pipe_as_ref ( read_apparent_size)
876+ . pipe ( Bytes :: new)
877+ } ;
878+
879+ let file_inode = |name : & str | {
880+ workspace
881+ . join ( "only-hardlinks/external" )
882+ . join ( name)
883+ . pipe_as_ref ( read_inode_number)
884+ . pipe ( InodeNumber :: from)
885+ } ;
886+
887+ let shared_paths = |file_names : & [ & str ] | {
888+ file_names
889+ . iter ( )
890+ . map ( |file_name| PathBuf :: from ( "only-hardlinks/external" ) . join ( file_name) )
891+ . collect :: < HashSet < _ > > ( )
892+ . pipe ( LinkPathListReflection )
893+ } ;
894+
895+ let actual_size = tree. size ;
896+ let expected_size = inode_size ( "only-hardlinks/external" ) + file_size * files_per_branch;
897+ assert_eq ! ( actual_size, expected_size) ;
898+
899+ let actual_shared_details: Vec < _ > = tree
900+ . shared
901+ . details
902+ . as_ref ( )
903+ . unwrap ( )
904+ . iter ( )
905+ . cloned ( )
906+ . collect ( ) ;
907+ let expected_shared_details = ( 0 ..files_per_branch)
908+ . par_bridge ( )
909+ . map ( |index| ReflectionEntry {
910+ ino : file_inode ( & format ! ( "linkX-{index}.txt" ) ) ,
911+ size : file_size,
912+ links : 2 ,
913+ paths : shared_paths ( & [ & format ! ( "linkX-{index}.txt" ) ] ) ,
914+ } )
915+ . collect :: < Vec < _ > > ( )
916+ . into_sorted_by_key ( |item : & ReflectionEntry < Bytes > | u64:: from ( item. ino ) ) ;
917+ assert_eq ! ( actual_shared_details, expected_shared_details) ;
918+
919+ let actual_shared_summary = tree. shared . summary ;
920+ let expected_shared_summary = Summary :: default ( )
921+ . with_inodes ( files_per_branch)
922+ . with_exclusive_inodes ( 0 )
923+ . with_all_links ( 2 * files_per_branch as u64 )
924+ . with_detected_links ( files_per_branch)
925+ . with_exclusive_links ( 0 )
926+ . with_shared_size ( files_per_branch * file_size)
927+ . with_exclusive_shared_size ( Bytes :: new ( 0 ) )
928+ . pipe ( Some ) ;
929+ assert_eq ! ( actual_shared_summary, expected_shared_summary) ;
930+
931+ let visualization = Command :: new ( PDU )
932+ . with_current_dir ( & workspace)
933+ . with_arg ( "--quantity=apparent-size" )
934+ . with_arg ( "--deduplicate-hardlinks" )
935+ . with_arg ( "only-hardlinks/external" )
936+ . pipe ( stdio)
937+ . output ( )
938+ . expect ( "spawn command" )
939+ . pipe ( stdout_text) ;
940+
941+ eprintln ! ( "STDOUT:\n {visualization}" ) ;
942+
943+ let actual_hardlinks_summary = visualization
944+ . lines ( )
945+ . skip_while ( |line| !line. starts_with ( "Hardlinks detected!" ) )
946+ . join ( "\n " ) ;
947+ let expected_hardlinks_summary = {
948+ use parallel_disk_usage:: size:: Size ;
949+ use std:: fmt:: Write ;
950+ let mut summary = String :: new ( ) ;
951+ writeln ! (
952+ summary,
953+ "Hardlinks detected! All hardlinks within this tree have links without" ,
954+ )
955+ . unwrap ( ) ;
956+ writeln ! ( summary, "* Number of shared inodes: {files_per_branch}" ) . unwrap ( ) ;
957+ writeln ! (
958+ summary,
959+ "* Total number of links: {total} total, {detected} detected" ,
960+ total = expected_shared_summary. unwrap( ) . all_links,
961+ detected = expected_shared_summary. unwrap( ) . detected_links,
962+ )
963+ . unwrap ( ) ;
964+ writeln ! (
965+ summary,
966+ "* Total shared size: {}" ,
967+ expected_shared_summary
968+ . unwrap( )
969+ . shared_size
970+ . display( BytesFormat :: MetricUnits ) ,
971+ )
972+ . unwrap ( ) ;
973+ summary
974+ } ;
975+ assert_eq ! (
976+ actual_hardlinks_summary. trim_end( ) ,
977+ expected_hardlinks_summary. trim_end( ) ,
978+ ) ;
979+ }
0 commit comments