From 9030f59976c778f83a09051786406876e946c6a7 Mon Sep 17 00:00:00 2001 From: FantasyTeddy Date: Fri, 8 Mar 2024 17:35:08 +0100 Subject: [PATCH] feat: add testability for drives --- .../IMockFileDataAccessor.cs | 16 +++ .../MockDriveData.cs | 76 +++++++++++ .../MockDriveInfo.cs | 109 +++++++++++----- .../MockDriveInfoFactory.cs | 9 +- .../MockFileSystem.cs | 44 +++++++ .../PathVerifier.cs | 33 +++++ .../DriveInfoBase.cs | 122 +++--------------- .../MockDriveInfoTests.cs | 119 +++++++++++++++++ .../MockFileSystemTests.cs | 12 ++ 9 files changed, 395 insertions(+), 145 deletions(-) create mode 100644 src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveData.cs diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs index 533e28be0..03b2af206 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/IMockFileDataAccessor.cs @@ -23,6 +23,13 @@ public interface IMockFileDataAccessor : IFileSystem /// The file. if the file does not exist. MockFileData GetFile(string path); + /// + /// Gets a drive. + /// + /// The name of the drive to get. + /// The drive. if the drive does not exist. + MockDriveData GetDrive(string name); + /// /// void AddFile(string path, MockFileData mockFile); @@ -31,6 +38,10 @@ public interface IMockFileDataAccessor : IFileSystem /// void AddDirectory(string path); + /// + /// + void AddDrive(string name, MockDriveData mockDrive); + /// /// void AddFileFromEmbeddedResource(string path, Assembly resourceAssembly, string embeddedResourcePath); @@ -74,6 +85,11 @@ public interface IMockFileDataAccessor : IFileSystem /// IEnumerable AllDirectories { get; } + /// + /// Gets the names of all drives. + /// + IEnumerable AllDrives { get; } + /// /// Gets a helper for string operations. /// diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveData.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveData.cs new file mode 100644 index 000000000..9d3dc169e --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveData.cs @@ -0,0 +1,76 @@ + +namespace System.IO.Abstractions.TestingHelpers +{ + /// + /// The class represents the associated data of a drive. + /// +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + public class MockDriveData + { + /// + /// Initializes a new instance of the class. + /// + public MockDriveData() + { + IsReady = true; + } + + /// + /// Initializes a new instance of the class by copying the given . + /// + /// The template instance. + /// Thrown if is . + public MockDriveData(MockDriveData template) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + AvailableFreeSpace = template.AvailableFreeSpace; + DriveFormat = template.DriveFormat; + DriveType = template.DriveType; + IsReady = template.IsReady; + TotalFreeSpace = template.TotalFreeSpace; + TotalSize = template.TotalSize; + VolumeLabel = template.VolumeLabel; + } + + /// + /// Gets or sets the amount of available free space of the , in bytes. + /// + public long AvailableFreeSpace { get; set; } + + /// + /// Gets or sets the name of the file system of the , such as NTFS or FAT32. + /// + public string DriveFormat { get; set; } + + /// + /// Gets or sets the drive type of the , such as CD-ROM, removable, network, or fixed. + /// + public DriveType DriveType { get; set; } + + /// + /// Gets or sets the value that indicates whether the is ready. + /// + public bool IsReady { get; set; } + + /// + /// Gets or sets the total amount of free space available on the , in bytes. + /// + public long TotalFreeSpace { get; set; } + + /// + /// Gets or sets the total size of storage space on the , in bytes. + /// + public long TotalSize { get; set; } + + /// + /// Gets or sets the volume label of the . + /// + public string VolumeLabel { get; set; } + } +} diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfo.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfo.cs index 60cc6dbe1..fcacd5472 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfo.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfo.cs @@ -7,51 +7,60 @@ public class MockDriveInfo : DriveInfoBase { private readonly IMockFileDataAccessor mockFileDataAccessor; + private readonly string name; /// public MockDriveInfo(IMockFileDataAccessor mockFileDataAccessor, string name) : base(mockFileDataAccessor?.FileSystem) { this.mockFileDataAccessor = mockFileDataAccessor ?? throw new ArgumentNullException(nameof(mockFileDataAccessor)); + this.name = mockFileDataAccessor.PathVerifier.NormalizeDriveName(name); + } - if (name == null) + /// + public override long AvailableFreeSpace + { + get { - throw new ArgumentNullException(nameof(name)); + var mockDriveData = GetMockDriveData(); + return mockDriveData.AvailableFreeSpace; } + } - const string DRIVE_SEPARATOR = @":\"; - - if (name.Length == 1 - || (name.Length == 2 && name[1] == ':') - || (name.Length == 3 && mockFileDataAccessor.StringOperations.EndsWith(name, DRIVE_SEPARATOR))) + /// + public override string DriveFormat + { + get { - name = name[0] + DRIVE_SEPARATOR; + var mockDriveData = GetMockDriveData(); + return mockDriveData.DriveFormat; } - else - { - mockFileDataAccessor.PathVerifier.CheckInvalidPathChars(name); - name = mockFileDataAccessor.Path.GetPathRoot(name); + } - if (string.IsNullOrEmpty(name) || mockFileDataAccessor.StringOperations.StartsWith(name, @"\\")) - { - throw new ArgumentException( - @"Object must be a root directory (""C:\"") or a drive letter (""C"")."); - } + /// + public override DriveType DriveType + { + get + { + var mockDriveData = GetMockDriveData(); + return mockDriveData.DriveType; } - - Name = name; - IsReady = true; } /// - public new long AvailableFreeSpace { get; set; } - /// - public new string DriveFormat { get; set; } - /// - public new DriveType DriveType { get; set; } - /// - public new bool IsReady { get; protected set; } + public override bool IsReady + { + get + { + var mockDriveData = GetMockDriveData(); + return mockDriveData.IsReady; + } + } + /// - public override string Name { get; protected set; } + public override string Name + { + get { return name; } + } /// public override IDirectoryInfo RootDirectory @@ -63,16 +72,50 @@ public override IDirectoryInfo RootDirectory } /// - public override string ToString() + public override long TotalFreeSpace { - return Name; + get + { + var mockDriveData = GetMockDriveData(); + return mockDriveData.TotalFreeSpace; + } } /// - public new long TotalFreeSpace { get; protected set; } + public override long TotalSize + { + get + { + var mockDriveData = GetMockDriveData(); + return mockDriveData.TotalSize; + } + } + /// - public new long TotalSize { get; protected set; } + public override string VolumeLabel + { + get + { + var mockDriveData = GetMockDriveData(); + return mockDriveData.VolumeLabel; + } + set + { + var mockDriveData = GetMockDriveData(); + mockDriveData.VolumeLabel = value; + } + } + /// - public override string VolumeLabel { get; set; } + public override string ToString() + { + return Name; + } + + private MockDriveData GetMockDriveData() + { + return mockFileDataAccessor.GetDrive(name) + ?? throw CommonExceptions.FileNotFound(name); + } } } diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfoFactory.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfoFactory.cs index 0ebc24070..9fafc539d 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfoFactory.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockDriveInfoFactory.cs @@ -23,15 +23,8 @@ public IFileSystem FileSystem /// public IDriveInfo[] GetDrives() { - var driveLetters = new HashSet(new DriveEqualityComparer(mockFileSystem)); - foreach (var path in mockFileSystem.AllPaths) - { - var pathRoot = mockFileSystem.Path.GetPathRoot(path); - driveLetters.Add(pathRoot); - } - var result = new List(); - foreach (string driveLetter in driveLetters) + foreach (string driveLetter in mockFileSystem.AllDrives) { try { diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index 1e977d517..160907511 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -17,6 +17,7 @@ public class MockFileSystem : FileSystemBase, IMockFileDataAccessor private const string TEMP_DIRECTORY = @"C:\temp"; private readonly IDictionary files; + private readonly IDictionary drives; private readonly PathVerifier pathVerifier; #if FEATURE_SERIALIZABLE [NonSerialized] @@ -58,6 +59,7 @@ public MockFileSystem(IDictionary files, MockFileSystemOpt StringOperations = new StringOperations(XFS.IsUnixPlatform()); pathVerifier = new PathVerifier(this); this.files = new Dictionary(StringOperations.Comparer); + drives = new Dictionary(StringOperations.Comparer); Path = new MockPath(this, defaultTempDirectory); File = new MockFile(this); @@ -188,6 +190,16 @@ public MockFileData GetFile(string path) return GetFileWithoutFixingPath(path); } + /// + public MockDriveData GetDrive(string name) + { + name = PathVerifier.NormalizeDriveName(name); + lock (drives) + { + return drives.TryGetValue(name, out var result) ? result : null; + } + } + private void SetEntry(string path, MockFileData mockFile) { path = FixPath(path, true).TrimSlashes(); @@ -322,6 +334,16 @@ public void AddDirectory(string path) var s = StringOperations.EndsWith(fixedPath, separator) ? fixedPath : fixedPath + separator; SetEntry(s, new MockDirectoryData()); } + + lock (drives) + { + var driveLetter = Path.GetPathRoot(fixedPath); + + if (!drives.ContainsKey(driveLetter)) + { + drives[driveLetter] = new MockDriveData(); + } + } } /// @@ -359,6 +381,16 @@ public void AddFilesFromEmbeddedNamespace(string path, Assembly resourceAssembly } } + /// + public void AddDrive(string name, MockDriveData mockDrive) + { + name = PathVerifier.NormalizeDriveName(name); + lock (drives) + { + drives[name] = mockDrive; + } + } + /// public void MoveDirectory(string sourcePath, string destPath) { @@ -483,6 +515,18 @@ public IEnumerable AllDirectories } } + /// + public IEnumerable AllDrives + { + get + { + lock (drives) + { + return drives.Keys.ToArray(); + } + } + } + [OnDeserializing] private void OnDeserializing(StreamingContext c) { diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index a635399b1..f6975a4ca 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs @@ -118,5 +118,38 @@ public void CheckInvalidPathChars(string path, bool checkAdditional = false) throw CommonExceptions.IllegalCharactersInPath(); } } + + /// + /// Determines the normalized drive name used for drive identification. + /// + public string NormalizeDriveName(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + const string DRIVE_SEPARATOR = @":\"; + + if (name.Length == 1 + || (name.Length == 2 && name[1] == ':') + || (name.Length == 3 && _mockFileDataAccessor.StringOperations.EndsWith(name, DRIVE_SEPARATOR))) + { + name = name[0] + DRIVE_SEPARATOR; + } + else + { + CheckInvalidPathChars(name); + name = _mockFileDataAccessor.Path.GetPathRoot(name); + + if (string.IsNullOrEmpty(name) || _mockFileDataAccessor.StringOperations.StartsWith(name, @"\\")) + { + throw new ArgumentException( + @"Object must be a root directory (""C:\"") or a drive letter (""C"")."); + } + } + + return name; + } } } diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/DriveInfoBase.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/DriveInfoBase.cs index 15830733a..14ac46b76 100644 --- a/src/TestableIO.System.IO.Abstractions.Wrappers/DriveInfoBase.cs +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/DriveInfoBase.cs @@ -20,118 +20,32 @@ internal DriveInfoBase() { } /// public IFileSystem FileSystem { get; } - /// - /// - /// Gets or sets the amount of available free space on a drive, in bytes. - /// - /// The amount of free space available on the drive, in bytes. - /// - /// This property indicates the amount of free space available on the drive. - /// Note that this number may be different from the TotalFreeSpace number because this property takes into account disk quotas. - /// - /// Thrown if the access to the drive information is denied. - /// Thrown if an I/O error occurred (for example, a disk error or a drive was not ready). - public virtual long AvailableFreeSpace { get; protected set; } + /// + public abstract long AvailableFreeSpace { get; } - /// - /// - /// Gets or sets the name of the file system, such as NTFS or FAT32. - /// - /// - /// Use DriveFormat to determine what formatting a drive uses. - /// - /// The name of the file system on the specified drive. - /// Thrown if the access to the drive information is denied. - /// Thrown if the drive does not exist or is not mapped. - /// Thrown if an I/O error occurred (for example, a disk error or a drive was not ready). - public virtual string DriveFormat { get; protected set; } + /// + public abstract string DriveFormat { get; } - /// - /// - /// Gets or sets the drive type, such as CD-ROM, removable, network, or fixed. - /// - /// One of the enumeration values that specifies a drive type. - /// - /// The DriveType property indicates whether a drive is one of the following: CDRom, Fixed, Network, NoRootDirectory, Ram, Removable, or Unknown. - /// These values are described in the DriveType enumeration. - /// - public virtual DriveType DriveType { get; protected set; } + /// + public abstract DriveType DriveType { get; } - /// - /// - /// Gets or sets a value indicating whether a drive is ready. - /// - /// - /// if the drive is ready; if the drive is not ready. - /// - /// - /// IsReady indicates whether a drive is ready. - /// For example, it indicates whether a CD is in a CD drive or whether a removable storage device is ready for read/write operations. - /// If you do not test whether a drive is ready, and it is not ready, querying the drive using will raise an IOException. - /// Do not rely on IsReady to avoid catching exceptions from other members such as TotalSize, TotalFreeSpace, and . - /// Between the time that your code checks IsReady and then accesses one of the other properties (even if the access occurs immediately after the check), - /// a drive may have been disconnected or a disk may have been removed. - /// - public virtual bool IsReady { get; protected set; } + /// + public abstract bool IsReady { get; } - /// - /// - /// Gets or sets the name of a drive, such as C:\. - /// - /// The name of the drive. - /// - /// This property is the name assigned to the drive, such as C:\ or E:\. - /// - public virtual string Name { get; protected set; } + /// + public abstract string Name { get; } - /// - /// - /// Gets or sets the root directory of a drive. - /// - /// An object that contains the root directory of the drive. - public virtual IDirectoryInfo RootDirectory { get; protected set; } + /// + public abstract IDirectoryInfo RootDirectory { get; } - /// - /// - /// Gets or sets the total amount of free space available on a drive, in bytes. - /// - /// The total free space available on a drive, in bytes. - /// This property indicates the total amount of free space available on the drive, not just what is available to the current user. - /// Thrown if the access to the drive information is denied. - /// Thrown if the drive does not exist or is not mapped. - /// Thrown if an I/O error occurred (for example, a disk error or a drive was not ready). - public virtual long TotalFreeSpace { get; protected set; } + /// + public abstract long TotalFreeSpace { get; } - /// - /// - /// Gets or sets the total size of storage space on a drive, in bytes. - /// - /// The total size of the drive, in bytes. - /// - /// This property indicates the total size of the drive in bytes, not just what is available to the current user. - /// - /// Thrown if the access to the drive information is denied. - /// Thrown if the drive does not exist or is not mapped. - /// Thrown if an I/O error occurred (for example, a disk error or a drive was not ready). - public virtual long TotalSize { get; protected set; } + /// + public abstract long TotalSize { get; } - /// - /// - /// Gets or sets the volume label of a drive. - /// - /// The volume label. - /// - /// The label length is determined by the operating system. For example, NTFS allows a volume label to be up to 32 characters long. Note that is a valid VolumeLabel. - /// - /// Thrown if an I/O error occurred (for example, a disk error or a drive was not ready). - /// Thrown if the drive does not exist or is not mapped. - /// Thrown if the caller does not have the required permission. - /// - /// Thrown if the volume label is being set on a network or CD-ROM drive - /// -or- - /// Access to the drive information is denied. - /// - public virtual string VolumeLabel { get; set; } + /// + public abstract string VolumeLabel { get; set; } /// /// Converts a into a . diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDriveInfoTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDriveInfoTests.cs index 5b95cea13..94e119df3 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDriveInfoTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDriveInfoTests.cs @@ -84,5 +84,124 @@ public void MockDriveInfo_ToString_ShouldReturnTheDrivePath(string path, string // Assert Assert.That(mockDriveInfo.ToString(), Is.EqualTo(expectedPath)); } + + [Test] + public void MockDriveInfo_AvailableFreeSpace_ShouldReturnAvailableFreeSpaceOfDriveInMemoryFileSystem() + { + // Arrange + var availableFreeSpace = 1024L; + var driveData = new MockDriveData { AvailableFreeSpace = availableFreeSpace }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.AvailableFreeSpace; + + // Assert + Assert.That(result, Is.EqualTo(availableFreeSpace)); + } + + [Test] + public void MockDriveInfo_DriveFormat_ShouldReturnDriveFormatOfDriveInMemoryFileSystem() + { + // Arrange + var driveFormat = "NTFS"; + var driveData = new MockDriveData { DriveFormat = driveFormat }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.DriveFormat; + + // Assert + Assert.That(result, Is.EqualTo(driveFormat)); + } + + [Test] + public void MockDriveInfo_DriveType_ShouldReturnDriveTypeOfDriveInMemoryFileSystem() + { + // Arrange + var driveType = DriveType.Fixed; + var driveData = new MockDriveData { DriveType = driveType }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.DriveType; + + // Assert + Assert.That(result, Is.EqualTo(driveType)); + } + + [TestCase(true)] + [TestCase(false)] + public void MockDriveInfo_IsReady_ShouldReturnIsReadyOfDriveInMemoryFileSystem(bool isReady) + { + // Arrange + var driveData = new MockDriveData { IsReady = isReady }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.IsReady; + + // Assert + Assert.That(result, Is.EqualTo(isReady)); + } + + [Test] + public void MockDriveInfo_TotalFreeSpace_ShouldReturnTotalFreeSpaceOfDriveInMemoryFileSystem() + { + // Arrange + var totalFreeSpace = 4096L; + var driveData = new MockDriveData { TotalFreeSpace = totalFreeSpace }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.TotalFreeSpace; + + // Assert + Assert.That(result, Is.EqualTo(totalFreeSpace)); + } + + [Test] + public void MockDriveInfo_TotalSize_ShouldReturnTotalSizeOfDriveInMemoryFileSystem() + { + // Arrange + var totalSize = 8192L; + var driveData = new MockDriveData { TotalSize = totalSize }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.TotalSize; + + // Assert + Assert.That(result, Is.EqualTo(totalSize)); + } + + [Test] + public void MockDriveInfo_VolumeLabel_ShouldReturnVolumeLabelOfDriveInMemoryFileSystem() + { + // Arrange + var volumeLabel = "Windows"; + var driveData = new MockDriveData { VolumeLabel = volumeLabel }; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive("C:", driveData); + var mockDriveInfo = new MockDriveInfo(fileSystem, "C:"); + + // Act + var result = mockDriveInfo.VolumeLabel; + + // Assert + Assert.That(result, Is.EqualTo(volumeLabel)); + } } } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs index 0294d092d..28fed6d61 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs @@ -207,6 +207,18 @@ public void MockFileSystem_AddDirectory_ShouldThrowExceptionIfDirectoryIsReadOnl Assert.Throws(action); } + [Test] + public void MockFileSystem_AddDrive_ShouldExist() + { + string name = @"D:\"; + var fileSystem = new MockFileSystem(); + fileSystem.AddDrive(name, new MockDriveData()); + + var actualResults = fileSystem.DriveInfo.GetDrives().Select(d => d.Name); + + Assert.That(actualResults, Does.Contain(name)); + } + [Test] public void MockFileSystem_DriveInfo_ShouldNotThrowAnyException() {