diff --git a/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs b/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs index fe0a8db8..d841ffc9 100644 --- a/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs +++ b/src/ByteSync.Client/Interfaces/Controls/Inventories/IFileSystemInspector.cs @@ -1,10 +1,12 @@ using System.IO; +using ByteSync.Business.Inventories; using ByteSync.Common.Business.Misc; namespace ByteSync.Interfaces.Controls.Inventories; public interface IFileSystemInspector { + FileSystemEntryKind ClassifyEntry(FileSystemInfo fsi); bool IsHidden(FileSystemInfo fsi, OSPlatforms os); bool IsSystemAttribute(FileInfo fileInfo); bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os); diff --git a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs index ac3675fa..1d69dcd1 100644 --- a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs +++ b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs @@ -1,4 +1,5 @@ using System.IO; +using ByteSync.Business.Inventories; using ByteSync.Common.Business.Misc; using ByteSync.Interfaces.Controls.Inventories; @@ -7,6 +8,48 @@ namespace ByteSync.Services.Inventories; public class FileSystemInspector : IFileSystemInspector { private const int FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS = 4194304; + private readonly IPosixFileTypeClassifier _posixFileTypeClassifier; + + public FileSystemInspector(IPosixFileTypeClassifier? posixFileTypeClassifier = null) + { + _posixFileTypeClassifier = posixFileTypeClassifier ?? new PosixFileTypeClassifier(); + } + + public FileSystemEntryKind ClassifyEntry(FileSystemInfo fsi) + { + if (fsi is null) + { + return FileSystemEntryKind.Unknown; + } + + if (HasLinkTarget(fsi) || SafeIsReparsePoint(fsi)) + { + return FileSystemEntryKind.Symlink; + } + + if (!OperatingSystem.IsWindows()) + { + try + { + var posixKind = _posixFileTypeClassifier.ClassifyPosixEntry(fsi.FullName); + if (posixKind != FileSystemEntryKind.Unknown) + { + return posixKind; + } + } + catch (Exception) + { + return FileSystemEntryKind.Unknown; + } + } + + return fsi switch + { + DirectoryInfo => FileSystemEntryKind.Directory, + FileInfo => FileSystemEntryKind.RegularFile, + _ => FileSystemEntryKind.Unknown + }; + } public bool IsHidden(FileSystemInfo fsi, OSPlatforms os) { @@ -53,4 +96,33 @@ public bool IsRecallOnDataAccess(FileInfo fileInfo) { return (((int)fileInfo.Attributes) & FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS) == FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; } + + private static bool HasLinkTarget(FileSystemInfo fsi) + { + try + { + return fsi switch + { + FileInfo fileInfo => fileInfo.LinkTarget != null, + DirectoryInfo directoryInfo => directoryInfo.LinkTarget != null, + _ => false + }; + } + catch (Exception) + { + return false; + } + } + + private bool SafeIsReparsePoint(FileSystemInfo fsi) + { + try + { + return IsReparsePoint(fsi); + } + catch (Exception) + { + return false; + } + } } diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs index 83e04c88..3677bcc8 100644 --- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs +++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs @@ -46,8 +46,7 @@ public InventoryBuilder(SessionMember sessionMember, DataNode dataNode, SessionS InventorySaver = inventorySaver; InventoryFileAnalyzer = inventoryFileAnalyzer; - FileSystemInspector = fileSystemInspector ?? new FileSystemInspector(); - PosixFileTypeClassifier = posixFileTypeClassifier ?? new PosixFileTypeClassifier(); + FileSystemInspector = fileSystemInspector ?? new FileSystemInspector(posixFileTypeClassifier); } private Inventory InstantiateInventory() @@ -90,8 +89,6 @@ private Inventory InstantiateInventory() private IFileSystemInspector FileSystemInspector { get; } - private IPosixFileTypeClassifier PosixFileTypeClassifier { get; } - private bool IgnoreHidden { get { return SessionSettings is { ExcludeHiddenFiles: true }; } @@ -268,12 +265,7 @@ private void ProcessSubDirectories(InventoryPart inventoryPart, DirectoryInfo di try { - if (IsReparsePoint(subDirectory)) - { - RecordSkippedEntry(inventoryPart, subDirectory, SkipReason.Symlink, FileSystemEntryKind.Symlink); - - continue; - } + DoAnalyze(inventoryPart, subDirectory, cancellationToken); } catch (UnauthorizedAccessException ex) { @@ -296,8 +288,6 @@ private void ProcessSubDirectories(InventoryPart inventoryPart, DirectoryInfo di continue; } - - DoAnalyze(inventoryPart, subDirectory, cancellationToken); } } @@ -408,7 +398,7 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo) private bool TryHandleFileSkip(InventoryPart inventoryPart, FileInfo fileInfo, bool isRoot) { - var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(fileInfo.FullName); + var entryKind = FileSystemInspector.ClassifyEntry(fileInfo); if (entryKind == FileSystemEntryKind.Symlink) { RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); @@ -441,13 +431,6 @@ private bool TryHandleFileSkip(InventoryPart inventoryPart, FileInfo fileInfo, b } } - if (IsReparsePoint(fileInfo)) - { - RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); - - return true; - } - if (!FileSystemInspector.Exists(fileInfo)) { RecordSkippedEntry(inventoryPart, fileInfo, SkipReason.NotFound); @@ -480,7 +463,7 @@ private void DoAnalyze(InventoryPart inventoryPart, DirectoryInfo directoryInfo, return; } - var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(directoryInfo.FullName); + var entryKind = FileSystemInspector.ClassifyEntry(directoryInfo); if (entryKind == FileSystemEntryKind.Symlink) { RecordSkippedEntry(inventoryPart, directoryInfo, SkipReason.Symlink, FileSystemEntryKind.Symlink); @@ -576,32 +559,6 @@ private bool ShouldIgnoreHiddenFile(FileInfo fileInfo) return null; } - private bool IsReparsePoint(FileInfo fileInfo) - { - if (FileSystemInspector.IsReparsePoint(fileInfo)) - { - _logger.LogWarning( - "File {File} is ignored because it has flag 'ReparsePoint'. It might be a symbolic link", - fileInfo.FullName); - - return true; - } - - return false; - } - - private bool IsReparsePoint(DirectoryInfo directoryInfo) - { - if (FileSystemInspector.IsReparsePoint(directoryInfo)) - { - _logger.LogWarning("Directory {Directory} is ignored because it has flag 'ReparsePoint'", directoryInfo.FullName); - - return true; - } - - return false; - } - private bool IsRecallOnDataAccess(FileInfo fileInfo) { return FileSystemInspector.IsRecallOnDataAccess(fileInfo); @@ -748,4 +705,4 @@ private void AddFileSystemDescription(InventoryPart inventoryPart, FileSystemDes } } } -} \ No newline at end of file +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs new file mode 100644 index 00000000..698e1b5c --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs @@ -0,0 +1,122 @@ +using ByteSync.Business.Inventories; +using ByteSync.Interfaces.Controls.Inventories; +using ByteSync.Services.Inventories; +using FluentAssertions; +using Moq; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Services.Inventories; + +public class FileSystemInspectorTests +{ + [Test] + public void ClassifyEntry_ReturnsDirectory_ForDirectoryInfo() + { + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); + var inspector = new FileSystemInspector(posix.Object); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + + try + { + var result = inspector.ClassifyEntry(tempDirectory); + + result.Should().Be(FileSystemEntryKind.Directory); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } + + [Test] + public void ClassifyEntry_ReturnsRegularFile_ForFileInfo() + { + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); + var inspector = new FileSystemInspector(posix.Object); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); + File.WriteAllText(tempFilePath, "x"); + var fileInfo = new FileInfo(tempFilePath); + + try + { + var result = inspector.ClassifyEntry(fileInfo); + + result.Should().Be(FileSystemEntryKind.RegularFile); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } + + [Test] + public void ClassifyEntry_ReturnsSymlink_WhenLinkTargetExists() + { + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); + var inspector = new FileSystemInspector(posix.Object); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var targetPath = Path.Combine(tempDirectory.FullName, "target.txt"); + File.WriteAllText(targetPath, "x"); + var linkPath = Path.Combine(tempDirectory.FullName, "link.txt"); + + try + { + try + { + File.CreateSymbolicLink(linkPath, targetPath); + } + catch (Exception ex) + { + Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}"); + } + + var result = inspector.ClassifyEntry(new FileInfo(linkPath)); + + result.Should().Be(FileSystemEntryKind.Symlink); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } + + [Test] + [Platform(Include = "Linux,MacOsX")] + public void ClassifyEntry_ReturnsPosixSpecialKind_WhenClassifierProvidesOne() + { + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Fifo); + var inspector = new FileSystemInspector(posix.Object); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var tempFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); + File.WriteAllText(tempFilePath, "x"); + var fileInfo = new FileInfo(tempFilePath); + + try + { + var result = inspector.ClassifyEntry(fileInfo); + + result.Should().Be(FileSystemEntryKind.Fifo); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } + + [Test] + public void ClassifyEntry_ReturnsUnknown_ForNullEntry() + { + var posix = new Mock(MockBehavior.Strict); + posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Unknown); + var inspector = new FileSystemInspector(posix.Object); + + var result = inspector.ClassifyEntry(null!); + + result.Should().Be(FileSystemEntryKind.Unknown); + } +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs index d6d7e941..888b1a7f 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/InventoryBuilderInspectorTests.cs @@ -83,11 +83,24 @@ private InventoryBuilder CreateBuilder(IFileSystemInspector inspector, Inventory var builder = CreateBuilder(inspector, processData, posixFileTypeClassifier); return (builder, processData); } + + private static void SetupDefaultClassification(Mock inspector) + { + inspector + .Setup(i => i.ClassifyEntry(It.IsAny())) + .Returns(fsi => fsi switch + { + DirectoryInfo => FileSystemEntryKind.Directory, + FileInfo => FileSystemEntryKind.RegularFile, + _ => FileSystemEntryKind.Unknown + }); + } [Test] public async Task Hidden_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); @@ -112,6 +125,7 @@ public async Task Hidden_Root_File_Is_Analyzed() public async Task System_Root_File_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(true); @@ -136,6 +150,7 @@ public async Task System_Root_File_Is_Analyzed() public async Task Hidden_Root_Directory_Is_Analyzed() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(true); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); @@ -163,6 +178,7 @@ public async Task Hidden_Root_Directory_Is_Analyzed() public async Task Hidden_Child_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.Is(fi => fi.Name == "hidden.txt"), It.IsAny())) .Returns(true); @@ -194,6 +210,7 @@ public async Task Hidden_Child_File_Is_Ignored() public async Task System_Child_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); @@ -223,6 +240,7 @@ public async Task System_Child_File_Is_Ignored() public async Task Noise_Child_File_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.Is(fi => fi.Name == "thumbs.db"), It.IsAny())) @@ -254,6 +272,7 @@ public async Task Noise_Child_File_Is_Recorded() public async Task System_Child_File_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); @@ -283,6 +302,7 @@ public async Task System_Child_File_Is_Recorded() public async Task Offline_Root_File_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(false); insp.Setup(i => i.Exists(It.IsAny())).Returns(true); insp.Setup(i => i.IsOffline(It.IsAny())).Returns(true); @@ -306,9 +326,9 @@ public async Task Offline_Root_File_Is_Recorded() public async Task Posix_Symlink_File_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); - var posix = new Mock(MockBehavior.Strict); - posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); - var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + SetupDefaultClassification(insp); + insp.Setup(i => i.ClassifyEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); + var (builder, processData) = CreateBuilderWithData(insp.Object); var filePath = Path.Combine(TestDirectory.FullName, "posix_symlink.txt"); await File.WriteAllTextAsync(filePath, "x"); @@ -327,9 +347,9 @@ public async Task Posix_Symlink_File_Is_Recorded() public async Task Posix_Special_File_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); - var posix = new Mock(MockBehavior.Strict); - posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.BlockDevice); - var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + SetupDefaultClassification(insp); + insp.Setup(i => i.ClassifyEntry(It.IsAny())).Returns(FileSystemEntryKind.BlockDevice); + var (builder, processData) = CreateBuilderWithData(insp.Object); var filePath = Path.Combine(TestDirectory.FullName, "posix_special.txt"); await File.WriteAllTextAsync(filePath, "x"); @@ -349,9 +369,9 @@ public async Task Posix_Special_File_Is_Recorded() public async Task Posix_Symlink_Directory_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); - var posix = new Mock(MockBehavior.Strict); - posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); - var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + SetupDefaultClassification(insp); + insp.Setup(i => i.ClassifyEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); + var (builder, processData) = CreateBuilderWithData(insp.Object); var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_posix_symlink")); @@ -369,9 +389,9 @@ public async Task Posix_Symlink_Directory_Is_Recorded() public async Task Posix_Special_Directory_Is_Recorded() { var insp = new Mock(MockBehavior.Strict); - var posix = new Mock(MockBehavior.Strict); - posix.Setup(p => p.ClassifyPosixEntry(It.IsAny())).Returns(FileSystemEntryKind.BlockDevice); - var (builder, processData) = CreateBuilderWithData(insp.Object, posix.Object); + SetupDefaultClassification(insp); + insp.Setup(i => i.ClassifyEntry(It.IsAny())).Returns(FileSystemEntryKind.BlockDevice); + var (builder, processData) = CreateBuilderWithData(insp.Object); var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_posix_special")); @@ -391,10 +411,11 @@ public async Task Posix_Special_Directory_Is_Recorded() public async Task Reparse_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); + insp.Setup(i => i.ClassifyEntry(It.IsAny())).Returns(FileSystemEntryKind.Symlink); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); - insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(true); var builder = CreateBuilder(insp.Object); var filePath = Path.Combine(TestDirectory.FullName, "c.txt"); @@ -412,6 +433,7 @@ public async Task Reparse_File_Is_Ignored() public async Task ExistsFalse_File_Is_Ignored() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); @@ -436,6 +458,7 @@ public async Task ExistsFalse_File_Is_Ignored() public async Task UnauthorizedAccess_Adds_Inaccessible_FileDescription() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); // Directory is readable insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); @@ -470,6 +493,7 @@ public async Task UnauthorizedAccess_Adds_Inaccessible_FileDescription() public async Task DirectoryNotFound_Adds_Inaccessible_FileDescription() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())) .Throws(new DirectoryNotFoundException("parent missing")); @@ -501,6 +525,7 @@ public async Task DirectoryNotFound_Adds_Inaccessible_FileDescription() public async Task IOException_Adds_Inaccessible_FileDescription() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())) .Throws(new IOException("io error")); @@ -532,6 +557,7 @@ public async Task IOException_Adds_Inaccessible_FileDescription() public async Task Directory_IOException_Marked_Inaccessible_And_Skipped() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); @@ -544,14 +570,10 @@ public async Task Directory_IOException_Marked_Inaccessible_And_Skipped() var root = Directory.CreateDirectory(Path.Combine(TestDirectory.FullName, "root_dir_io")); var sub = Directory.CreateDirectory(Path.Combine(root.FullName, "BadSub")); - // Throw IOException for this specific subdirectory when checking reparse - insp.Setup(i => i.IsReparsePoint(It.Is(fsi => fsi.FullName == sub.FullName))) + // Throw IOException for this specific subdirectory during classification + insp.Setup(i => i.ClassifyEntry(It.Is(fsi => fsi.FullName == sub.FullName))) .Throws(new IOException("dir io error")); - // Default to not reparse otherwise - insp.Setup(i => i.IsReparsePoint(It.Is(fsi => fsi.FullName != sub.FullName))) - .Returns(false); - var okFile = Path.Combine(root.FullName, "ok.txt"); await File.WriteAllTextAsync(okFile, "x"); @@ -572,6 +594,7 @@ public async Task Directory_IOException_Marked_Inaccessible_And_Skipped() public async Task Directory_ReparsePoint_Is_Skipped() { var insp = new Mock(MockBehavior.Strict); + SetupDefaultClassification(insp); insp.Setup(i => i.IsHidden(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsNoiseFileName(It.IsAny(), It.IsAny())).Returns(false); insp.Setup(i => i.IsSystemAttribute(It.IsAny())).Returns(false); @@ -585,7 +608,12 @@ public async Task Directory_ReparsePoint_Is_Skipped() // Define the desired value and then record the behavior based on that value. var reparseDir = sub.FullName; - insp.Setup(i => i.IsReparsePoint(It.IsAny())).Returns(fsi => fsi.FullName == reparseDir); + insp.Setup(i => i.ClassifyEntry(It.IsAny())) + .Returns(fsi => fsi.FullName == reparseDir + ? FileSystemEntryKind.Symlink + : fsi is DirectoryInfo + ? FileSystemEntryKind.Directory + : FileSystemEntryKind.RegularFile); var builder = CreateBuilder(insp.Object);