diff --git a/src/device.rs b/src/device.rs index 1ef5a12b..545cce12 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,3 +1,8 @@ +use derive_more::{Display, From, Into, LowerHex, Octal, UpperHex}; + +#[cfg(feature = "json")] +use serde::{Deserialize, Serialize}; + /// Whether to cross device boundary into a different filesystem. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceBoundary { @@ -15,3 +20,22 @@ impl DeviceBoundary { } } } + +/// The device number of a filesystem. +#[derive( + Debug, Display, LowerHex, UpperHex, Octal, Clone, Copy, PartialEq, Eq, Hash, From, Into, +)] +#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +pub struct DeviceNumber(u64); + +/// POSIX-exclusive functions. +#[cfg(unix)] +impl DeviceNumber { + /// Get device number of a [`std::fs::Metadata`]. + #[inline] + pub fn get(stats: &std::fs::Metadata) -> Self { + use pipe_trait::Pipe; + use std::os::unix::fs::MetadataExt; + stats.dev().pipe(DeviceNumber) + } +} diff --git a/src/hardlink/aware.rs b/src/hardlink/aware.rs index 36dedbc0..ad3b5977 100644 --- a/src/hardlink/aware.rs +++ b/src/hardlink/aware.rs @@ -4,6 +4,7 @@ use super::{ }; use crate::{ data_tree::DataTree, + device::DeviceNumber, inode::InodeNumber, os_string_display::OsStringDisplay, reporter::{event::HardlinkDetection, Event, Reporter}, @@ -20,7 +21,7 @@ use std::{convert::Infallible, fmt::Debug, os::unix::fs::MetadataExt, path::Path /// accurately reflect the real size of their containers. #[derive(Debug, SmartDefault, Clone, AsRef, AsMut, From, Into)] pub struct Aware { - /// Map an inode number to its size and detected paths. + /// Map each file (identified by inode number and device number) to its size and detected paths. record: HardlinkList, } @@ -82,8 +83,9 @@ where })); let ino = InodeNumber::get(stats); + let dev = DeviceNumber::get(stats); self.record - .add(ino, size, links, path) + .add(ino, dev, size, links, path) .map_err(ReportHardlinksError::AddToRecord) } } diff --git a/src/hardlink/hardlink_list.rs b/src/hardlink/hardlink_list.rs index c955de8e..8879567d 100644 --- a/src/hardlink/hardlink_list.rs +++ b/src/hardlink/hardlink_list.rs @@ -9,7 +9,7 @@ pub use summary::Summary; pub use Reflection as HardlinkListReflection; pub use Summary as SharedLinkSummary; -use crate::{hardlink::LinkPathList, inode::InodeNumber, size}; +use crate::{device::DeviceNumber, hardlink::LinkPathList, inode::InodeNumber, size}; use dashmap::DashMap; use derive_more::{Display, Error}; use smart_default::SmartDefault; @@ -20,6 +20,15 @@ use pipe_trait::Pipe; #[cfg(any(unix, test))] use std::path::Path; +/// Internal key used to uniquely identify an inode across all filesystems. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +struct InodeKey { + /// Inode number within the device. + ino: InodeNumber, + /// Device number of the filesystem the inode belongs to. + dev: DeviceNumber, +} + /// Map value in [`HardlinkList`]. #[derive(Debug, Clone)] struct Value { @@ -38,8 +47,8 @@ struct Value { /// [`Reflection`] which implement these traits. #[derive(Debug, SmartDefault, Clone)] pub struct HardlinkList( - /// Map an inode number to its size, number of links, and detected paths. - DashMap>, + /// Map an inode key (device + inode number) to its size, number of links, and detected paths. + DashMap>, ); impl HardlinkList { @@ -64,31 +73,39 @@ impl HardlinkList { } } -/// Error that occurs when a different size was detected for the same [`ino`][ino]. +/// Error that occurs when a different size was detected for the same [`ino`][ino] and [`dev`][dev]. /// /// /// [ino]: https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.ino +/// +/// [dev]: https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.dev #[derive(Debug, Display, Error)] #[cfg_attr(test, derive(PartialEq, Eq))] #[display(bound(Size: Debug))] -#[display("Size for inode {ino} changed from {recorded:?} to {detected:?}")] +#[display("Size for inode {ino} on device {dev} changed from {recorded:?} to {detected:?}")] pub struct SizeConflictError { pub ino: InodeNumber, + pub dev: DeviceNumber, pub recorded: Size, pub detected: Size, } -/// Error that occurs when a different [`nlink`][nlink] was detected for the same [`ino`][ino]. +/// Error that occurs when a different [`nlink`][nlink] was detected for the same [`ino`][ino] and [`dev`][dev]. /// /// /// [nlink]: https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.nlink /// /// [ino]: https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.ino +/// +/// [dev]: https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.dev #[derive(Debug, Display, Error)] #[cfg_attr(test, derive(PartialEq, Eq))] -#[display("Number of links of inode {ino} changed from {recorded:?} to {detected:?}")] +#[display( + "Number of links of inode {ino} on device {dev} changed from {recorded:?} to {detected:?}" +)] pub struct NumberOfLinksConflictError { pub ino: InodeNumber, + pub dev: DeviceNumber, pub recorded: u64, pub detected: u64, } @@ -112,17 +129,20 @@ where pub(crate) fn add( &self, ino: InodeNumber, + dev: DeviceNumber, size: Size, links: u64, path: &Path, ) -> Result<(), AddError> { + let key = InodeKey { ino, dev }; let mut assertions = Ok(()); self.0 - .entry(ino) + .entry(key) .and_modify(|recorded| { if size != recorded.size { assertions = Err(AddError::SizeConflict(SizeConflictError { ino, + dev, recorded: recorded.size, detected: size, })); @@ -133,6 +153,7 @@ where assertions = Err(AddError::NumberOfLinksConflict( NumberOfLinksConflictError { ino, + dev, recorded: recorded.links, detected: links, }, diff --git a/src/hardlink/hardlink_list/iter.rs b/src/hardlink/hardlink_list/iter.rs index 4b2c1b3c..539ec5c7 100644 --- a/src/hardlink/hardlink_list/iter.rs +++ b/src/hardlink/hardlink_list/iter.rs @@ -1,5 +1,5 @@ -use super::{HardlinkList, Value}; -use crate::{hardlink::LinkPathList, inode::InodeNumber}; +use super::{HardlinkList, InodeKey, Value}; +use crate::{device::DeviceNumber, hardlink::LinkPathList, inode::InodeNumber}; use dashmap::{iter::Iter as DashIter, mapref::multiple::RefMulti}; use pipe_trait::Pipe; @@ -7,7 +7,7 @@ use pipe_trait::Pipe; #[derive(derive_more::Debug)] #[debug(bound())] #[debug("Iter(..)")] -pub struct Iter<'a, Size>(DashIter<'a, InodeNumber, Value>); +pub struct Iter<'a, Size>(DashIter<'a, InodeKey, Value>); impl HardlinkList { /// Iterate over the recorded entries. @@ -20,7 +20,7 @@ impl HardlinkList { #[derive(derive_more::Debug)] #[debug(bound())] #[debug("Item(..)")] -pub struct Item<'a, Size>(RefMulti<'a, InodeNumber, Value>); +pub struct Item<'a, Size>(RefMulti<'a, InodeKey, Value>); impl<'a, Size> Iterator for Iter<'a, Size> { type Item = Item<'a, Size>; @@ -33,7 +33,13 @@ impl<'a, Size> Item<'a, Size> { /// The inode number of the file. #[inline] pub fn ino(&self) -> InodeNumber { - *self.0.key() + self.0.key().ino + } + + /// The device number of the filesystem the inode belongs to. + #[inline] + pub fn dev(&self) -> DeviceNumber { + self.0.key().dev } /// Size of the file. diff --git a/src/hardlink/hardlink_list/reflection.rs b/src/hardlink/hardlink_list/reflection.rs index c190041b..7a6659c2 100644 --- a/src/hardlink/hardlink_list/reflection.rs +++ b/src/hardlink/hardlink_list/reflection.rs @@ -1,5 +1,5 @@ -use super::{HardlinkList, Value}; -use crate::{hardlink::LinkPathListReflection, inode::InodeNumber}; +use super::{HardlinkList, InodeKey, Value}; +use crate::{device::DeviceNumber, hardlink::LinkPathListReflection, inode::InodeNumber}; use dashmap::DashMap; use derive_more::{Display, Error, Into, IntoIterator}; use into_sorted::IntoSortedUnstable; @@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize}; /// internal content. /// /// **Guarantees:** -/// * Every inode number is unique. -/// * The internal list is always sorted by inode numbers. +/// * Every pair of an inode number and a device number is unique. +/// * The internal list is always sorted by pairs of an inode number and a device number. /// /// **Equality:** `Reflection` implements `PartialEq` and `Eq` traits. /// @@ -50,6 +50,8 @@ impl Reflection { pub struct ReflectionEntry { /// The inode number of the file. pub ino: InodeNumber, + /// Device number of the filesystem the inode belongs to. + pub dev: DeviceNumber, /// Size of the file. pub size: Size, /// Total number of links of the file, both listed (in [`Self::paths`]) and unlisted. @@ -61,34 +63,47 @@ pub struct ReflectionEntry { impl ReflectionEntry { /// Create a new entry. #[inline] - fn new(ino: InodeNumber, Value { size, links, paths }: Value) -> Self { + fn new(InodeKey { ino, dev }: InodeKey, Value { size, links, paths }: Value) -> Self { let paths = paths.into(); ReflectionEntry { ino, + dev, size, links, paths, } } - /// Dissolve [`ReflectionEntry`] into a pair of [`InodeNumber`] and [`Value`]. + /// Dissolve [`ReflectionEntry`] into a pair of [`InodeKey`] and [`Value`]. #[inline] - fn dissolve(self) -> (InodeNumber, Value) { + fn dissolve(self) -> (InodeKey, Value) { let ReflectionEntry { ino, + dev, size, links, paths, } = self; let paths = paths.into(); - (ino, Value { size, links, paths }) + (InodeKey { ino, dev }, Value { size, links, paths }) + } + + /// Sorting key to be used in the "sort by key" family of functions. + /// + /// Sort by the inode number first, then by the device number. + /// + /// This function returns a pair of 2 `u64`s instead of a pair of 2 wrapper + /// types because we prefer them not to have to implement `Ord`. + #[inline] + fn sorting_key(&self) -> (u64, u64) { + (u64::from(self.ino), u64::from(self.dev)) } } impl From>> for Reflection { - /// Sort the list by inode numbers, then create the reflection. + /// Sort the list by inode numbers and device numbers, then create the reflection. fn from(list: Vec>) -> Self { - list.into_sorted_unstable_by_key(|entry| u64::from(entry.ino)) + list.into_sorted_unstable_by_key(ReflectionEntry::sorting_key) .pipe(Reflection) } } @@ -96,7 +111,7 @@ impl From>> for Reflection { impl From> for Reflection { fn from(HardlinkList(list): HardlinkList) -> Self { list.into_iter() - .map(|(ino, value)| ReflectionEntry::new(ino, value)) + .map(|(key, value)| ReflectionEntry::new(key, value)) .collect::>() .pipe(Reflection::from) } @@ -107,9 +122,19 @@ impl From> for Reflection { #[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum ConversionError { - /// When the source has duplicated inode numbers. - #[display("Inode number {_0} is duplicated")] - DuplicatedInode(#[error(not(source))] InodeNumber), + /// When the source has a duplicated `(inode, device)` pair. + #[display("Inode {_0} on device {_1} is duplicated")] + DuplicatedInode(InodeNumber, DeviceNumber), +} + +impl ConversionError { + /// Convenient function to convert an [`InodeKey`] into a [`ConversionError::DuplicatedInode`]. + /// + /// We don't embed [`InodeKey`] directly into [`ConversionError::DuplicatedInode`] because of + /// their difference in visibility: One is private, the other public. + fn duplicated_inode(InodeKey { ino, dev }: InodeKey) -> Self { + ConversionError::DuplicatedInode(ino, dev) + } } impl TryFrom> for HardlinkList { @@ -118,9 +143,9 @@ impl TryFrom> for HardlinkList { let map = DashMap::with_capacity(entries.len()); for entry in entries { - let (ino, value) = entry.dissolve(); - if map.insert(ino, value).is_some() { - return ino.pipe(ConversionError::DuplicatedInode).pipe(Err); + let (key, value) = entry.dissolve(); + if map.insert(key, value).is_some() { + return key.pipe(ConversionError::duplicated_inode).pipe(Err); } } diff --git a/src/hardlink/hardlink_list/test.rs b/src/hardlink/hardlink_list/test.rs index 8e6878d2..04240f86 100644 --- a/src/hardlink/hardlink_list/test.rs +++ b/src/hardlink/hardlink_list/test.rs @@ -3,21 +3,22 @@ use crate::size::Bytes; use pipe_trait::Pipe; use pretty_assertions::{assert_eq, assert_ne}; -const TABLE: &[(u64, u64, u64, &str)] = &[ - (241, 3652, 1, "a"), - (569, 2210, 1, "b"), - (110, 2350, 3, "c"), - (110, 2350, 3, "c1"), - (778, 1110, 1, "d"), - (274, 6060, 2, "e"), - (274, 6060, 2, "e1"), - (883, 4530, 1, "f"), +const TABLE: &[(u64, u64, u64, u64, &str)] = &[ + // ino, dev, size, links, path + (241, 0, 3652, 1, "a"), + (569, 0, 2210, 1, "b"), + (110, 0, 2350, 3, "c"), + (110, 0, 2350, 3, "c1"), + (778, 0, 1110, 1, "d"), + (274, 0, 6060, 2, "e"), + (274, 0, 6060, 2, "e1"), + (883, 0, 4530, 1, "f"), ]; fn add(list: HardlinkList) -> HardlinkList { let values = TABLE[ROW]; - let (ino, size, links, path) = values; - if let Err(error) = list.add(ino.into(), size.into(), links, path.as_ref()) { + let (ino, dev, size, links, path) = values; + if let Err(error) = list.add(ino.into(), dev.into(), size.into(), links, path.as_ref()) { panic!("Failed to add {values:?} (index: {ROW}) to the list: {error}"); } list @@ -119,13 +120,14 @@ fn insertion_difference_cause_inequality() { #[test] fn detect_size_change() { let list = HardlinkList::::new(); - list.add(123.into(), 100.into(), 1, "a".as_ref()) + list.add(123.into(), 0.into(), 100.into(), 1, "a".as_ref()) .expect("add the first path"); let actual = list - .add(123.into(), 110.into(), 1, "b".as_ref()) + .add(123.into(), 0.into(), 110.into(), 1, "b".as_ref()) .expect_err("add the second path"); let expected = AddError::SizeConflict(SizeConflictError { ino: 123.into(), + dev: 0.into(), recorded: 100.into(), detected: 110.into(), }); @@ -135,15 +137,48 @@ fn detect_size_change() { #[test] fn detect_number_of_links_change() { let list = HardlinkList::::new(); - list.add(123.into(), 100.into(), 1, "a".as_ref()) + list.add(123.into(), 0.into(), 100.into(), 1, "a".as_ref()) .expect("add the first path"); let actual = list - .add(123.into(), 100.into(), 2, "b".as_ref()) + .add(123.into(), 0.into(), 100.into(), 2, "b".as_ref()) .expect_err("add the second path"); let expected = AddError::NumberOfLinksConflict(NumberOfLinksConflictError { ino: 123.into(), + dev: 0.into(), recorded: 1, detected: 2, }); assert_eq!(actual, expected); } + +#[test] +fn same_ino_on_different_devices_are_treated_separately() { + let list = HardlinkList::::new(); + + // dev=1, ino=100 — first filesystem + list.add(100.into(), 1.into(), 50.into(), 2, "dev1/file_a".as_ref()) + .expect("add dev1/file_a"); + list.add(100.into(), 1.into(), 50.into(), 2, "dev1/file_b".as_ref()) + .expect("add dev1/file_b (same dev+ino → same inode group)"); + + // dev=2, ino=100 — second filesystem, coincidentally same inode number + list.add(100.into(), 2.into(), 80.into(), 2, "dev2/file_c".as_ref()) + .expect("add dev2/file_c (different dev → separate inode group)"); + list.add(100.into(), 2.into(), 80.into(), 2, "dev2/file_d".as_ref()) + .expect("add dev2/file_d (same dev+ino → same inode group as file_c)"); + + // Each device should produce its own entry, so the list should have 2 entries. + assert_eq!(list.len(), 2, "expected one entry per (ino, dev) pair"); + + let reflection = list.into_reflection(); + assert_eq!(reflection.len(), 2); + + // Sorted by (ino, dev), so dev=1 comes first. + let entries: Vec<_> = reflection.iter().collect(); + assert_eq!(entries[0].dev, 1.into()); + assert_eq!(entries[0].ino, 100.into()); + assert_eq!(entries[0].paths.len(), 2); + assert_eq!(entries[1].dev, 2.into()); + assert_eq!(entries[1].ino, 100.into()); + assert_eq!(entries[1].paths.len(), 2); +} diff --git a/src/json_data/schema_version.rs b/src/json_data/schema_version.rs index a392f1d6..6ac50bf4 100644 --- a/src/json_data/schema_version.rs +++ b/src/json_data/schema_version.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::convert::TryFrom; /// Content of [`SchemaVersion`]. -pub const SCHEMA_VERSION: &str = "2024-11-02"; +pub const SCHEMA_VERSION: &str = "2026-04-02"; /// Verifying schema version. #[derive(Debug, Clone, Copy)] diff --git a/tests/_utils.rs b/tests/_utils.rs index 3a9c785a..61314ef5 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -583,3 +583,13 @@ pub fn read_inode_number(path: &Path) -> u64 { .unwrap_or_else(|error| panic!("Can't read metadata at {path:?}: {error}")) .ino() } + +/// Read [dev](std::os::unix::fs::MetadataExt::dev) of a path. +#[cfg(unix)] +pub fn read_device_number(path: &Path) -> parallel_disk_usage::device::DeviceNumber { + use std::os::unix::fs::MetadataExt; + path.pipe(symlink_metadata) + .unwrap_or_else(|error| panic!("Can't read metadata at {path:?}: {error}")) + .dev() + .into() +} diff --git a/tests/hardlinks_deduplication.rs b/tests/hardlinks_deduplication.rs index 73274458..c33a010e 100644 --- a/tests/hardlinks_deduplication.rs +++ b/tests/hardlinks_deduplication.rs @@ -85,6 +85,8 @@ fn simple_tree_with_some_hardlinks() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |suffices: &[&str]| { suffices .iter() @@ -190,6 +192,7 @@ fn simple_tree_with_some_hardlinks() { .collect(); let expected_shared_details = [ ReflectionEntry { + dev, ino: file_inode("one-internal-hardlink.txt"), size: file_size("one-internal-hardlink.txt"), links: 1 + 1, @@ -199,6 +202,7 @@ fn simple_tree_with_some_hardlinks() { ]), }, ReflectionEntry { + dev, ino: file_inode("two-internal-hardlinks.txt"), size: file_size("two-internal-hardlinks.txt"), links: 1 + 2, @@ -209,12 +213,14 @@ fn simple_tree_with_some_hardlinks() { ]), }, ReflectionEntry { + dev, ino: file_inode("one-external-hardlink.txt"), size: file_size("one-external-hardlink.txt"), links: 1 + 1, paths: shared_paths(&["sources/one-external-hardlink.txt"]), }, ReflectionEntry { + dev, ino: file_inode("one-internal-one-external-hardlinks.txt"), size: file_size("one-internal-one-external-hardlinks.txt"), links: 1 + 1 + 1, @@ -338,6 +344,8 @@ fn multiple_hardlinks_to_a_single_file() { .pipe_as_ref(read_inode_number) .pipe(InodeNumber::from); + let dev = read_device_number(&workspace); + let actual_size = tree.size; let expected_size = workspace .pipe_as_ref(read_apparent_size) @@ -374,6 +382,7 @@ fn multiple_hardlinks_to_a_single_file() { .cloned() .collect(); let expected_shared_details = [ReflectionEntry { + dev, ino: file_inode, size: file_size, links: 1 + links, @@ -470,6 +479,8 @@ fn complex_tree_with_shared_and_unique_files() { .pipe(Bytes::new) }; + let dev = read_device_number(&workspace); + let actual_size = tree.size; // The following formula treat the first file as "real" and @@ -562,6 +573,7 @@ fn complex_tree_with_shared_and_unique_files() { .find(|item| starts_with_path(item, "some-hardlinks/file-0.txt")) .cloned(); let expected = Some(ReflectionEntry { + dev, ino: workspace .join("some-hardlinks/file-0.txt") .pipe_as_ref(read_inode_number) @@ -591,6 +603,7 @@ fn complex_tree_with_shared_and_unique_files() { .find(|item| starts_with_path(item, &format!("some-hardlinks/file-{file_index}.txt"))) .cloned(); let expected = Some(ReflectionEntry { + dev, ino: workspace .join(format!("some-hardlinks/file-{file_index}.txt")) .pipe_as_ref(read_inode_number) @@ -695,6 +708,8 @@ fn hardlinks_and_non_hardlinks() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |file_names: &[&str]| { file_names .iter() @@ -717,12 +732,14 @@ fn hardlinks_and_non_hardlinks() { .collect(); let expected_shared_details = [ ReflectionEntry { + dev, ino: file_inode("file-0.txt"), size: file_size, links: 3, paths: shared_paths(&["file-0.txt", "link0-file0.txt", "link1-file0.txt"]), }, ReflectionEntry { + dev, ino: file_inode("file-1.txt"), size: file_size, links: 2, @@ -730,24 +747,28 @@ fn hardlinks_and_non_hardlinks() { }, // ... file-2.txt and file-3.txt don't have hardlinks so they shouldn't appear here ... ReflectionEntry { + dev, ino: file_inode("file-4.txt"), size: file_size, links: 2, paths: shared_paths(&["file-4.txt"]), }, ReflectionEntry { + dev, ino: file_inode("file-5.txt"), size: file_size, links: 2, paths: shared_paths(&["file-5.txt"]), }, ReflectionEntry { + dev, ino: file_inode("file-6.txt"), size: file_size, links: 2, paths: shared_paths(&["file-6.txt"]), }, ReflectionEntry { + dev, ino: file_inode("file-7.txt"), size: file_size, links: 2, @@ -898,6 +919,8 @@ fn exclusive_hardlinks_only() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |file_names: &[&str]| { file_names .iter() @@ -921,6 +944,7 @@ fn exclusive_hardlinks_only() { let expected_shared_details = (0..files_per_branch) .par_bridge() .map(|index| ReflectionEntry { + dev, ino: file_inode(&format!("file-{index}.txt")), size: file_size, links: 2, @@ -1024,6 +1048,8 @@ fn exclusive_only_and_external_only_hardlinks() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |file_names: &[&str]| { file_names .iter() @@ -1050,6 +1076,7 @@ fn exclusive_only_and_external_only_hardlinks() { (0..(files_per_branch / 2)) .par_bridge() .map(|index| ReflectionEntry { + dev, ino: file_inode(&format!("link0-{index}.txt")), size: file_size, links: 2, @@ -1060,6 +1087,7 @@ fn exclusive_only_and_external_only_hardlinks() { ((files_per_branch / 2)..files_per_branch) .par_bridge() .map(|index| ReflectionEntry { + dev, ino: file_inode(&format!("link0-{index}.txt")), size: file_size, links: 2, @@ -1187,6 +1215,8 @@ fn external_hardlinks_only() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |file_names: &[&str]| { file_names .iter() @@ -1210,6 +1240,7 @@ fn external_hardlinks_only() { let expected_shared_details = (0..files_per_branch) .par_bridge() .map(|index| ReflectionEntry { + dev, ino: file_inode(&format!("linkX-{index}.txt")), size: file_size, links: 2, diff --git a/tests/hardlinks_deduplication_multi_args.rs b/tests/hardlinks_deduplication_multi_args.rs index 4851aff1..531dbd57 100644 --- a/tests/hardlinks_deduplication_multi_args.rs +++ b/tests/hardlinks_deduplication_multi_args.rs @@ -82,6 +82,8 @@ fn simple_tree_with_some_hardlinks() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |suffices: &[&str]| { suffices .iter() @@ -144,6 +146,7 @@ fn simple_tree_with_some_hardlinks() { .collect(); let expected_shared_details = [ ReflectionEntry { + dev, ino: file_inode("one-internal-hardlink.txt"), size: file_size("one-internal-hardlink.txt"), links: 1 + 1, @@ -153,6 +156,7 @@ fn simple_tree_with_some_hardlinks() { ]), }, ReflectionEntry { + dev, ino: file_inode("two-internal-hardlinks.txt"), size: file_size("two-internal-hardlinks.txt"), links: 1 + 2, @@ -163,12 +167,14 @@ fn simple_tree_with_some_hardlinks() { ]), }, ReflectionEntry { + dev, ino: file_inode("one-external-hardlink.txt"), size: file_size("one-external-hardlink.txt"), links: 1 + 1, paths: shared_paths(&["sources/one-external-hardlink.txt"]), }, ReflectionEntry { + dev, ino: file_inode("one-internal-one-external-hardlinks.txt"), size: file_size("one-internal-one-external-hardlinks.txt"), links: 1 + 1 + 1, @@ -295,6 +301,8 @@ fn multiple_hardlinks_to_a_single_file() { .pipe_as_ref(read_inode_number) .pipe(InodeNumber::from); + let dev = read_device_number(&workspace); + let actual_size = tree.size; let expected_size = file_size; assert_eq!(actual_size, expected_size); @@ -319,6 +327,7 @@ fn multiple_hardlinks_to_a_single_file() { .cloned() .collect(); let expected_shared_details = [ReflectionEntry { + dev, ino: file_inode, size: file_size, links: 1 + links, @@ -437,6 +446,8 @@ fn multiple_duplicated_arguments() { .pipe(InodeNumber::from) }; + let dev = read_device_number(&workspace); + let shared_paths = |suffices: &[&str]| { suffices .iter() @@ -501,6 +512,7 @@ fn multiple_duplicated_arguments() { .collect(); let expected_shared_details = [ ReflectionEntry { + dev, ino: file_inode("one-internal-hardlink.txt"), size: file_size("one-internal-hardlink.txt"), links: 1 + 1, @@ -510,6 +522,7 @@ fn multiple_duplicated_arguments() { ]), }, ReflectionEntry { + dev, ino: file_inode("two-internal-hardlinks.txt"), size: file_size("two-internal-hardlinks.txt"), links: 1 + 2, @@ -520,12 +533,14 @@ fn multiple_duplicated_arguments() { ]), }, ReflectionEntry { + dev, ino: file_inode("one-external-hardlink.txt"), size: file_size("one-external-hardlink.txt"), links: 1 + 1, paths: shared_paths(&["main/sources/one-external-hardlink.txt"]), }, ReflectionEntry { + dev, ino: file_inode("one-internal-one-external-hardlinks.txt"), size: file_size("one-internal-one-external-hardlinks.txt"), links: 1 + 1 + 1,