@@ -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