From ce67de26f09c919e8ab1a7f1c3e9dd0c0305bff7 Mon Sep 17 00:00:00 2001 From: t4m45 <122280036+t4m45@users.noreply.github.com> Date: Sat, 28 Dec 2024 19:55:47 +0100 Subject: [PATCH] feat: Add FileVersionInfo support (#1177) Added a wrapper for the immutable `FileVersionInfo`. For testing, the `IFileVersionInfo` can be stored in the `MockFileData` as a property. Since testing it is rarely neccessary, I didn't add a constructor for it but it's a mutable property, so it can be set anytime after initialization. A `MockFileVersionInfo` has also been added which is `mutable`. In a normal scenario, the `FileVersionInfo` can be accessed through the `FileVersionInfo` property of `IFileInfo`. The `MockFileSystem`'s `AddFile` method will initialize this version if it's null, and the values will be the default (Only it's FileName will be set). Since the `System.IO.FileInfo` does not contain a `FileVersionInfo` property, I excluded it from the `ApiParityTests` PS.: Sorry, seems like were some auto formats on my side which removed some extra spaces and the Visual Studio's Diff viewer did not show these before commit. I'm not really familiar with github, thats why I didn't try to revert them after commit. --- .../MockFileData.cs | 5 + .../MockFileInfo.cs | 4 +- .../MockFileSystem.cs | 7 +- .../MockFileVersionInfo.cs | 174 ++++++++++++++++ .../MockFileVersionInfoFactory.cs | 33 ++++ .../ProductVersionParser.cs | 99 ++++++++++ .../FileInfoBase.cs | 4 +- .../FileSystem.cs | 4 + .../FileSystemBase.cs | 3 + .../FileVersionInfoBase.cs | 106 ++++++++++ .../FileVersionInfoFactory.cs | 27 +++ .../FileVersionInfoWrapper.cs | 187 ++++++++++++++++++ .../IFileSystem.cs | 5 + .../IFileVersionInfo.cs | 89 +++++++++ .../IFileVersionInfoFactory.cs | 13 ++ .../MockFileSystemTests.cs | 23 ++- .../MockFileVersionInfoFactoryTests.cs | 43 ++++ .../MockFileVersionInfoTests.cs | 86 ++++++++ .../ProductVersionParserTests.cs | 107 ++++++++++ .../ApiParityTests.cs | 10 +- .../FileVersionInfoBaseConversionTests.cs | 27 +++ ...iParityTests.FileVersionInfo_.NET 6.0.snap | 4 + ...iParityTests.FileVersionInfo_.NET 7.0.snap | 4 + ...iParityTests.FileVersionInfo_.NET 8.0.snap | 4 + ....FileVersionInfo_.NET Framework 4.6.2.snap | 4 + version.json | 2 +- 26 files changed, 1063 insertions(+), 11 deletions(-) create mode 100644 src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfo.cs create mode 100644 src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfoFactory.cs create mode 100644 src/TestableIO.System.IO.Abstractions.TestingHelpers/ProductVersionParser.cs create mode 100644 src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoBase.cs create mode 100644 src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoFactory.cs create mode 100644 src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoWrapper.cs create mode 100644 src/TestableIO.System.IO.Abstractions/IFileVersionInfo.cs create mode 100644 src/TestableIO.System.IO.Abstractions/IFileVersionInfoFactory.cs create mode 100644 tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoFactoryTests.cs create mode 100644 tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoTests.cs create mode 100644 tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/ProductVersionParserTests.cs create mode 100644 tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/FileVersionInfoBaseConversionTests.cs create mode 100644 tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 6.0.snap create mode 100644 tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 7.0.snap create mode 100644 tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 8.0.snap create mode 100644 tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET Framework 4.6.2.snap diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs index 7fcf94514..a7cfebb65 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs @@ -119,6 +119,11 @@ public MockFileData(MockFileData template) /// public byte[] Contents { get; set; } + /// + /// Gets or sets the file version info of the + /// + public IFileVersionInfo FileVersionInfo { get; set; } + /// /// Gets or sets the string contents of the . /// diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs index 5356260f1..f9dd951f5 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs @@ -259,7 +259,7 @@ public override void Encrypt() var mockFileData = GetMockFileDataForWrite(); mockFileData.Attributes |= FileAttributes.Encrypted; } - + /// public override void MoveTo(string destFileName) { @@ -323,7 +323,7 @@ public override IFileInfo Replace(string destinationFileName, string destination mockFile.Replace(path, destinationFileName, destinationBackupFileName, ignoreMetadataErrors); return mockFileSystem.FileInfo.New(destinationFileName); } - + /// public override IDirectoryInfo Directory { diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs index 4199c5622..572461551 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs @@ -65,6 +65,7 @@ public MockFileSystem(IDictionary files, MockFileSystemOpt File = new MockFile(this); Directory = new MockDirectory(this, currentDirectory); FileInfo = new MockFileInfoFactory(this); + FileVersionInfo = new MockFileVersionInfoFactory(this); FileStream = new MockFileStreamFactory(this); DirectoryInfo = new MockDirectoryInfoFactory(this); DriveInfo = new MockDriveInfoFactory(this); @@ -98,6 +99,8 @@ public MockFileSystem(IDictionary files, MockFileSystemOpt /// public override IFileInfoFactory FileInfo { get; } /// + public override IFileVersionInfoFactory FileVersionInfo { get; } + /// public override IFileStreamFactory FileStream { get; } /// public override IPath Path { get; } @@ -252,6 +255,8 @@ public void AddFile(string path, MockFileData mockFile) AddDirectory(directoryPath); } + mockFile.FileVersionInfo ??= new MockFileVersionInfo(fixedPath); + SetEntry(fixedPath, mockFile); } @@ -568,7 +573,7 @@ private bool FileIsReadOnly(string path) } #if FEATURE_SERIALIZABLE - [Serializable] + [Serializable] #endif private class FileSystemEntry { diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfo.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfo.cs new file mode 100644 index 000000000..1eaac4f59 --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfo.cs @@ -0,0 +1,174 @@ +using System.Diagnostics; +using System.Text; + +namespace System.IO.Abstractions.TestingHelpers +{ + /// +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + public class MockFileVersionInfo : FileVersionInfoBase + { + /// + public MockFileVersionInfo( + string fileName, + string fileVersion = null, + string productVersion = null, + string fileDescription = null, + string productName = null, + string companyName = null, + string comments = null, + string internalName = null, + bool isDebug = false, + bool isPatched = false, + bool isPrivateBuild = false, + bool isPreRelease = false, + bool isSpecialBuild = false, + string language = null, + string legalCopyright = null, + string legalTrademarks = null, + string originalFilename = null, + string privateBuild = null, + string specialBuild = null) + { + FileName = fileName; + FileVersion = fileVersion; + ProductVersion = productVersion; + FileDescription = fileDescription; + ProductName = productName; + CompanyName = companyName; + Comments = comments; + InternalName = internalName; + IsDebug = isDebug; + IsPatched = isPatched; + IsPrivateBuild = isPrivateBuild; + IsPreRelease = isPreRelease; + IsSpecialBuild = isSpecialBuild; + Language = language; + LegalCopyright = legalCopyright; + LegalTrademarks = legalTrademarks; + OriginalFilename = originalFilename; + PrivateBuild = privateBuild; + SpecialBuild = specialBuild; + + if (Version.TryParse(fileVersion, out Version version)) + { + FileMajorPart = version.Major; + FileMinorPart = version.Minor; + FileBuildPart = version.Build; + FilePrivatePart = version.Revision; + } + + var parsedProductVersion = ProductVersionParser.Parse(productVersion); + + ProductMajorPart = parsedProductVersion.Major; + ProductMinorPart = parsedProductVersion.Minor; + ProductBuildPart = parsedProductVersion.Build; + ProductPrivatePart = parsedProductVersion.PrivatePart; + } + + /// + public override string FileName { get; } + + /// + public override string FileVersion { get; } + + /// + public override string ProductVersion { get; } + + /// + public override string FileDescription { get; } + + /// + public override string ProductName { get; } + + /// + public override string CompanyName { get; } + + /// + public override string Comments { get; } + + /// + public override string InternalName { get; } + + /// + public override bool IsDebug { get; } + + /// + public override bool IsPatched { get; } + + /// + public override bool IsPrivateBuild { get; } + + /// + public override bool IsPreRelease { get; } + + /// + public override bool IsSpecialBuild { get; } + + /// + public override string Language { get; } + + /// + public override string LegalCopyright { get; } + + /// + public override string LegalTrademarks { get; } + + /// + public override string OriginalFilename { get; } + + /// + public override string PrivateBuild { get; } + + /// + public override string SpecialBuild { get; } + + /// + public override int FileMajorPart { get; } + + /// + public override int FileMinorPart { get; } + + /// + public override int FileBuildPart { get; } + + /// + public override int FilePrivatePart { get; } + + /// + public override int ProductMajorPart { get; } + + /// + public override int ProductMinorPart { get; } + + /// + public override int ProductBuildPart { get; } + + /// + public override int ProductPrivatePart { get; } + + /// + public override string ToString() + { + // An initial capacity of 512 was chosen because it is large enough to cover + // the size of the static strings with enough capacity left over to cover + // average length property values. + var sb = new StringBuilder(512); + sb.Append("File: ").AppendLine(FileName); + sb.Append("InternalName: ").AppendLine(InternalName); + sb.Append("OriginalFilename: ").AppendLine(OriginalFilename); + sb.Append("FileVersion: ").AppendLine(FileVersion); + sb.Append("FileDescription: ").AppendLine(FileDescription); + sb.Append("Product: ").AppendLine(ProductName); + sb.Append("ProductVersion: ").AppendLine(ProductVersion); + sb.Append("Debug: ").AppendLine(IsDebug.ToString()); + sb.Append("Patched: ").AppendLine(IsPatched.ToString()); + sb.Append("PreRelease: ").AppendLine(IsPreRelease.ToString()); + sb.Append("PrivateBuild: ").AppendLine(IsPrivateBuild.ToString()); + sb.Append("SpecialBuild: ").AppendLine(IsSpecialBuild.ToString()); + sb.Append("Language: ").AppendLine(Language); + return sb.ToString(); + } + } +} diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfoFactory.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfoFactory.cs new file mode 100644 index 000000000..76278f0dc --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileVersionInfoFactory.cs @@ -0,0 +1,33 @@ +namespace System.IO.Abstractions.TestingHelpers +{ + /// +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + public class MockFileVersionInfoFactory : IFileVersionInfoFactory + { + private readonly IMockFileDataAccessor mockFileSystem; + + /// + public MockFileVersionInfoFactory(IMockFileDataAccessor mockFileSystem) + { + this.mockFileSystem = mockFileSystem ?? throw new ArgumentNullException(nameof(mockFileSystem)); + } + + /// + public IFileSystem FileSystem => mockFileSystem; + + /// + public IFileVersionInfo GetVersionInfo(string fileName) + { + MockFileData mockFileData = mockFileSystem.GetFile(fileName); + + if (mockFileData != null) + { + return mockFileData.FileVersionInfo; + } + + throw CommonExceptions.FileNotFound(fileName); + } + } +} diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/ProductVersionParser.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/ProductVersionParser.cs new file mode 100644 index 000000000..011de195c --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/ProductVersionParser.cs @@ -0,0 +1,99 @@ +using System.Reflection; +using System.Text.RegularExpressions; + +namespace System.IO.Abstractions.TestingHelpers +{ + /// + /// Provides functionality to parse a product version string into its major, minor, build, and private parts. + /// + internal static class ProductVersionParser + { + /// + /// Parses a product version string and extracts the numeric values for the major, minor, build, and private parts, + /// mimicking the behavior of the attribute. + /// + /// The product version string to parse. + /// + /// A object containing the parsed major, minor, build, and private parts. + /// If the input is invalid, returns a with all parts set to 0. + /// + /// + /// The method splits the input string into segments separated by dots ('.') and attempts to extract + /// the leading numeric value from each segment. A maximum of 4 segments are processed; if more than + /// 4 segments are present, all segments are ignored. Additionally, if a segment does not contain + /// a valid numeric part at its start or it contains more than just a number, the rest of the segments are ignored. + /// + public static ProductVersion Parse(string productVersion) + { + if (string.IsNullOrWhiteSpace(productVersion)) + { + return new(); + } + + var segments = productVersion.Split('.'); + if (segments.Length > 4) + { + // if more than 4 segments are present, all segments are ignored + return new(); + } + + var regex = new Regex(@"^\d+"); + + int[] parts = new int[4]; + + for (int i = 0; i < segments.Length; i++) + { + var match = regex.Match(segments[i]); + if (match.Success && int.TryParse(match.Value, out int number)) + { + parts[i] = number; + + if (match.Value != segments[i]) + { + // when a segment contains more than a number, the rest of the segments are ignored + break; + } + } + else + { + // when a segment is not valid, the rest of the segments are ignored + break; + } + } + + return new() + { + Major = parts[0], + Minor = parts[1], + Build = parts[2], + PrivatePart = parts[3] + }; + } + + /// + /// Represents a product version with numeric parts for major, minor, build, and private versions. + /// + public class ProductVersion + { + /// + /// Gets the major part of the version number + /// + public int Major { get; init; } + + /// + /// Gets the minor part of the version number + /// + public int Minor { get; init; } + + /// + /// Gets the build part of the version number + /// + public int Build { get; init; } + + /// + /// Gets the private part of the version number + /// + public int PrivatePart { get; init; } + } + } +} diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoBase.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoBase.cs index 1c8875fb0..ca4df775b 100644 --- a/src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoBase.cs +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoBase.cs @@ -36,7 +36,7 @@ internal FileInfoBase() { } /// public abstract void Encrypt(); - + /// public abstract void MoveTo(string destFileName); @@ -73,7 +73,7 @@ internal FileInfoBase() { } /// public abstract IFileInfo Replace(string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors); - + /// public abstract IDirectoryInfo Directory { get; } diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystem.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystem.cs index 80e7ae69e..1e9cfca2b 100644 --- a/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystem.cs +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystem.cs @@ -12,6 +12,7 @@ public FileSystem() DriveInfo = new DriveInfoFactory(this); DirectoryInfo = new DirectoryInfoFactory(this); FileInfo = new FileInfoFactory(this); + FileVersionInfo = new FileVersionInfoFactory(this); Path = new PathWrapper(this); File = new FileWrapper(this); Directory = new DirectoryWrapper(this); @@ -28,6 +29,9 @@ public FileSystem() /// public override IFileInfoFactory FileInfo { get; } + /// + public override IFileVersionInfoFactory FileVersionInfo { get; } + /// public override IFileStreamFactory FileStream { get; } diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystemBase.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystemBase.cs index 6fb508161..b851d9eaa 100644 --- a/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystemBase.cs +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileSystemBase.cs @@ -15,6 +15,9 @@ public abstract class FileSystemBase : IFileSystem /// public abstract IFileInfoFactory FileInfo { get; } + /// + public abstract IFileVersionInfoFactory FileVersionInfo { get; } + /// public abstract IFileStreamFactory FileStream { get; } diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoBase.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoBase.cs new file mode 100644 index 000000000..d8dd87683 --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoBase.cs @@ -0,0 +1,106 @@ +using System.Diagnostics; + +namespace System.IO.Abstractions +{ + /// +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + public abstract class FileVersionInfoBase : IFileVersionInfo + { + /// + public abstract string Comments { get; } + + /// + public abstract string CompanyName { get; } + + /// + public abstract int FileBuildPart { get; } + + /// + public abstract string FileDescription { get; } + + /// + public abstract int FileMajorPart { get; } + + /// + public abstract int FileMinorPart { get; } + + /// + public abstract string FileName { get; } + + /// + public abstract int FilePrivatePart { get; } + + /// + public abstract string FileVersion { get; } + + /// + public abstract string InternalName { get; } + + /// + public abstract bool IsDebug { get; } + + /// + public abstract bool IsPatched { get; } + + /// + public abstract bool IsPrivateBuild { get; } + + /// + public abstract bool IsPreRelease { get; } + + /// + public abstract bool IsSpecialBuild { get; } + + /// + public abstract string Language { get; } + + /// + public abstract string LegalCopyright { get; } + + /// + public abstract string LegalTrademarks { get; } + + /// + public abstract string OriginalFilename { get; } + + /// + public abstract string PrivateBuild { get; } + + /// + public abstract int ProductBuildPart { get; } + + /// + public abstract int ProductMajorPart { get; } + + /// + public abstract int ProductMinorPart { get; } + + /// + public abstract string ProductName { get; } + + /// + public abstract int ProductPrivatePart { get; } + + /// + public abstract string ProductVersion { get; } + + /// + public abstract string SpecialBuild { get; } + + /// + public static implicit operator FileVersionInfoBase(FileVersionInfo fileVersionInfo) + { + if (fileVersionInfo == null) + { + return null; + } + + return new FileVersionInfoWrapper(fileVersionInfo); + } + + /// + public new abstract string ToString(); + } +} diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoFactory.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoFactory.cs new file mode 100644 index 000000000..f3594a28f --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoFactory.cs @@ -0,0 +1,27 @@ +namespace System.IO.Abstractions +{ +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + internal class FileVersionInfoFactory : IFileVersionInfoFactory + { + private readonly IFileSystem fileSystem; + + /// + public FileVersionInfoFactory(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + /// + public IFileSystem FileSystem => fileSystem; + + /// + public IFileVersionInfo GetVersionInfo(string fileName) + { + Diagnostics.FileVersionInfo fileVersionInfo = Diagnostics.FileVersionInfo.GetVersionInfo(fileName); + + return new FileVersionInfoWrapper(fileVersionInfo); + } + } +} diff --git a/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoWrapper.cs b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoWrapper.cs new file mode 100644 index 000000000..999864475 --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions.Wrappers/FileVersionInfoWrapper.cs @@ -0,0 +1,187 @@ +using System.Diagnostics; + +namespace System.IO.Abstractions +{ + /// +#if FEATURE_SERIALIZABLE + [Serializable] +#endif + public class FileVersionInfoWrapper : FileVersionInfoBase + { + private readonly FileVersionInfo instance; + + /// + public FileVersionInfoWrapper(FileVersionInfo fileVersionInfo) + { + instance = fileVersionInfo; + } + + /// + public override string Comments + { + get { return instance.Comments; } + } + + /// + public override string CompanyName + { + get { return instance.CompanyName; } + } + + /// + public override int FileBuildPart + { + get { return instance.FileBuildPart; } + } + + /// + public override string FileDescription + { + get { return instance.FileDescription; } + } + + /// + public override int FileMajorPart + { + get { return instance.FileMajorPart; } + } + + /// + public override int FileMinorPart + { + get { return instance.FileMinorPart; } + } + + /// + public override string FileName + { + get { return instance.FileName; } + } + + /// + public override int FilePrivatePart + { + get { return instance.FilePrivatePart; } + } + + /// + public override string FileVersion + { + get { return instance.FileVersion; } + } + + /// + public override string InternalName + { + get { return instance.InternalName; } + } + + /// + public override bool IsDebug + { + get { return instance.IsDebug; } + } + + /// + public override bool IsPatched + { + get { return instance.IsPatched; } + } + + /// + public override bool IsPrivateBuild + { + get { return instance.IsPrivateBuild; } + } + + /// + public override bool IsPreRelease + { + get { return instance.IsPreRelease; } + } + + /// + public override bool IsSpecialBuild + { + get { return instance.IsSpecialBuild; } + } + + /// + public override string Language + { + get { return instance.Language; } + } + + /// + public override string LegalCopyright + { + get { return instance.LegalCopyright; } + } + + /// + public override string LegalTrademarks + { + get { return instance.LegalTrademarks; } + } + + /// + public override string OriginalFilename + { + get { return instance.OriginalFilename; } + } + + /// + public override string PrivateBuild + { + get { return instance.PrivateBuild; } + } + + /// + public override int ProductBuildPart + { + get { return instance.ProductBuildPart; } + } + + /// + public override int ProductMajorPart + { + get { return instance.ProductMajorPart; } + } + + /// + public override int ProductMinorPart + { + get { return instance.ProductMinorPart; } + } + + /// + public override string ProductName + { + get { return instance.ProductName; } + } + + /// + public override int ProductPrivatePart + { + get { return instance.ProductPrivatePart; } + } + + /// + public override string ProductVersion + { + get { return instance.ProductVersion; } + } + + /// + public override string SpecialBuild + { + get { return instance.SpecialBuild; } + } + + /// + public override string ToString() + { + return instance.ToString(); + } + } +} diff --git a/src/TestableIO.System.IO.Abstractions/IFileSystem.cs b/src/TestableIO.System.IO.Abstractions/IFileSystem.cs index d6f27d526..bc56d6246 100644 --- a/src/TestableIO.System.IO.Abstractions/IFileSystem.cs +++ b/src/TestableIO.System.IO.Abstractions/IFileSystem.cs @@ -30,6 +30,11 @@ public interface IFileSystem /// IFileInfoFactory FileInfo { get; } + /// + /// A factory for the creation of wrappers for . + /// + IFileVersionInfoFactory FileVersionInfo { get; } + /// /// A factory for the creation of wrappers for . /// diff --git a/src/TestableIO.System.IO.Abstractions/IFileVersionInfo.cs b/src/TestableIO.System.IO.Abstractions/IFileVersionInfo.cs new file mode 100644 index 000000000..d7840f2f4 --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions/IFileVersionInfo.cs @@ -0,0 +1,89 @@ +using System.Diagnostics; + +namespace System.IO.Abstractions +{ + /// + public interface IFileVersionInfo + { + /// + string? Comments { get; } + + /// + string? CompanyName { get; } + + /// + int FileBuildPart { get; } + + /// + string? FileDescription { get; } + + /// + int FileMajorPart { get; } + + /// + int FileMinorPart { get; } + + /// + string FileName { get; } + + /// + int FilePrivatePart { get; } + + /// + string? FileVersion { get; } + + /// + string? InternalName { get; } + + /// + bool IsDebug { get; } + + /// + bool IsPatched { get; } + + /// + bool IsPrivateBuild { get; } + + /// + bool IsPreRelease { get; } + + /// + bool IsSpecialBuild { get; } + + /// + string? Language { get; } + + /// + string? LegalCopyright { get; } + + /// + string? LegalTrademarks { get; } + + /// + string? OriginalFilename { get; } + + /// + string? PrivateBuild { get; } + + /// + int ProductBuildPart { get; } + + /// + int ProductMajorPart { get; } + + /// + int ProductMinorPart { get; } + + /// + string? ProductName { get; } + + /// + int ProductPrivatePart { get; } + + /// + string? ProductVersion { get; } + + /// + string? SpecialBuild { get; } + } +} \ No newline at end of file diff --git a/src/TestableIO.System.IO.Abstractions/IFileVersionInfoFactory.cs b/src/TestableIO.System.IO.Abstractions/IFileVersionInfoFactory.cs new file mode 100644 index 000000000..a1d9208eb --- /dev/null +++ b/src/TestableIO.System.IO.Abstractions/IFileVersionInfoFactory.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace System.IO.Abstractions +{ + /// + /// A factory for the creation of wrappers for in a . + /// + public interface IFileVersionInfoFactory : IFileSystemEntity + { + /// + IFileVersionInfo GetVersionInfo(string fileName); + } +} diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs index 4b681421c..1f88311c1 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileSystemTests.cs @@ -286,6 +286,21 @@ public void MockFileSystem_AddFile_ShouldMatchCapitalization_PartialMatch_Furthe Assert.That(fileSystem.AllDirectories.ToList(), Does.Contain(XFS.Path(@"C:\LOUD\SUBLOUD\new\SUBDirectory"))); } + [Test] + public void MockFileSystem_AddFile_InitializesMockFileDataFileVersionInfoIfNull() + { + // Arrange + var fileSystem = new MockFileSystem(); + + // Act + fileSystem.AddFile(XFS.Path(@"C:\file.txt"), string.Empty); + + // Assert + IFileVersionInfo fileVersionInfo = fileSystem.FileVersionInfo.GetVersionInfo(XFS.Path(@"C:\file.txt")); + Assert.That(fileVersionInfo, Is.Not.Null); + Assert.That(fileVersionInfo.FileName, Is.EqualTo(XFS.Path(@"C:\file.txt"))); + } + [Test] public void MockFileSystem_AddFileFromEmbeddedResource_ShouldAddTheFile() { @@ -435,7 +450,7 @@ public void MockFileSystem_Constructor_ThrowsForNonRootedCurrentDirectory() ); Assert.That(ae.ParamName, Is.EqualTo("currentDirectory")); } - + [Test] [WindowsOnly(WindowsSpecifics.Drives)] public void MockFileSystem_Constructor_ShouldSupportDifferentRootDrives() @@ -450,7 +465,7 @@ public void MockFileSystem_Constructor_ShouldSupportDifferentRootDrives() var cExists = fileSystem.Directory.Exists(@"c:\"); var zExists = fileSystem.Directory.Exists(@"z:\"); var dExists = fileSystem.Directory.Exists(@"d:\"); - + Assert.That(fileSystem, Is.Not.Null); Assert.That(cExists, Is.True); Assert.That(zExists, Is.True); @@ -484,9 +499,9 @@ public void MockFileSystem_Constructor_ShouldNotDuplicateDrives() [@"d:\foo\bar\"] = new MockDirectoryData(), [@"d:\"] = new MockDirectoryData() }); - + var drivesInfo = fileSystem.DriveInfo.GetDrives(); - + Assert.That(drivesInfo.Where(d => string.Equals(d.Name, @"D:\", StringComparison.InvariantCultureIgnoreCase)), Has.Exactly(1).Items); } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoFactoryTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoFactoryTests.cs new file mode 100644 index 000000000..9b10ac3a8 --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoFactoryTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using System.Collections.Generic; + +namespace System.IO.Abstractions.TestingHelpers.Tests +{ + [TestFixture] + public class MockFileVersionInfoFactoryTests + { + [Test] + public void MockFileVersionInfoFactory_GetVersionInfo_ShouldReturnTheFileVersionInfoOfTheMockFileData() + { + // Arrange + var fileVersionInfo = new MockFileVersionInfo(@"c:\a.txt"); + var fileSystem = new MockFileSystem(new Dictionary + { + { @"c:\a.txt", new MockFileData("Demo text content") { FileVersionInfo = fileVersionInfo } } + }); + + // Act + var result = fileSystem.FileVersionInfo.GetVersionInfo(@"c:\a.txt"); + + // Assert + Assert.That(result, Is.EqualTo(fileVersionInfo)); + } + + [Test] + public void MockFileVersionInfoFactory_GetVersionInfo_ShouldThrowFileNotFoundExceptionIfFileDoesNotExist() + { + // Arrange + var fileSystem = new MockFileSystem(new Dictionary + { + { @"c:\a.txt", new MockFileData("Demo text content") }, + { @"c:\a\b\c.txt", new MockFileData("Demo text content") }, + }); + + // Act + TestDelegate code = () => fileSystem.FileVersionInfo.GetVersionInfo(@"c:\foo.txt"); + + // Assert + Assert.Throws(code); + } + } +} diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoTests.cs new file mode 100644 index 000000000..307c7c1cc --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockFileVersionInfoTests.cs @@ -0,0 +1,86 @@ +using NUnit.Framework; + +namespace System.IO.Abstractions.TestingHelpers.Tests +{ + [TestFixture] + public class MockFileVersionInfoTests + { + [Test] + public void MockFileVersionInfo_ToString_ShouldReturnTheDefaultFormat() + { + // Arrange + var mockFileVersionInfo = new MockFileVersionInfo( + fileName: @"c:\b.txt", + fileVersion: "1.0.0.0", + productVersion: "1.0.0.0", + fileDescription: "b", + productName: "b", + companyName: null, + comments: null, + internalName: "b.txt", + isDebug: true, + isPatched: true, + isPrivateBuild: true, + isPreRelease: true, + isSpecialBuild: true, + language: "English", + legalCopyright: null, + legalTrademarks: null, + originalFilename: "b.txt", + privateBuild: null, + specialBuild: null); + + string expected = @"File: c:\b.txt +InternalName: b.txt +OriginalFilename: b.txt +FileVersion: 1.0.0.0 +FileDescription: b +Product: b +ProductVersion: 1.0.0.0 +Debug: True +Patched: True +PreRelease: True +PrivateBuild: True +SpecialBuild: True +Language: English +"; + + // Act & Assert + Assert.That(mockFileVersionInfo.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void MockFileVersionInfo_Constructor_ShouldSetFileAndProductVersionNumbersIfFileAndProductVersionAreNotNull() + { + // Arrange + var mockFileVersionInfo = new MockFileVersionInfo(@"c:\file.txt", fileVersion: "1.2.3.4", productVersion: "5.6.7.8"); + + // Assert + Assert.That(mockFileVersionInfo.FileMajorPart, Is.EqualTo(1)); + Assert.That(mockFileVersionInfo.FileMinorPart, Is.EqualTo(2)); + Assert.That(mockFileVersionInfo.FileBuildPart, Is.EqualTo(3)); + Assert.That(mockFileVersionInfo.FilePrivatePart, Is.EqualTo(4)); + Assert.That(mockFileVersionInfo.ProductMajorPart, Is.EqualTo(5)); + Assert.That(mockFileVersionInfo.ProductMinorPart, Is.EqualTo(6)); + Assert.That(mockFileVersionInfo.ProductBuildPart, Is.EqualTo(7)); + Assert.That(mockFileVersionInfo.ProductPrivatePart, Is.EqualTo(8)); + } + + [Test] + public void MockFileVersionInfo_Constructor_ShouldNotSetFileAndProductVersionNumbersIfFileAndProductVersionAreNull() + { + // Act + var mockFileVersionInfo = new MockFileVersionInfo(@"c:\a.txt"); + + // Assert + Assert.That(mockFileVersionInfo.FileMajorPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.FileMinorPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.FileBuildPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.FilePrivatePart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.ProductMajorPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.ProductMinorPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.ProductBuildPart, Is.EqualTo(0)); + Assert.That(mockFileVersionInfo.ProductPrivatePart, Is.EqualTo(0)); + } + } +} diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/ProductVersionParserTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/ProductVersionParserTests.cs new file mode 100644 index 000000000..980c8be5d --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/ProductVersionParserTests.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; + +namespace System.IO.Abstractions.TestingHelpers.Tests +{ + [TestFixture] + public class ProductVersionParserTests + { + [Test] + public void ProductVersionParser_Parse_ShouldIgnoreTheSegmentsWhenThereAreMoreThanFiveOfThem() + { + // Arrange + string productVersion = "1.2.3.4.5"; + + // Act + var parsedProductVersion = ProductVersionParser.Parse(productVersion); + + // Assert + Assert.Multiple(() => + { + Assert.That(parsedProductVersion.Major, Is.Zero); + Assert.That(parsedProductVersion.Minor, Is.Zero); + Assert.That(parsedProductVersion.Build, Is.Zero); + Assert.That(parsedProductVersion.PrivatePart, Is.Zero); + }); + } + + [Test] + [TestCase("test.2.3.4", 0, 0, 0, 0)] + [TestCase("1.test.3.4", 1, 0, 0, 0)] + [TestCase("1.2.test.4", 1, 2, 0, 0)] + [TestCase("1.2.3.test", 1, 2, 3, 0)] + public void ProductVersionParser_Parse_ShouldSkipTheRestOfTheSegmentsWhenOneIsNotValidNumber( + string productVersion, + int expectedMajor, + int expectedMinor, + int expectedBuild, + int expectedRevision) + { + // Act + var parsedProductVersion = ProductVersionParser.Parse(productVersion); + + // Assert + Assert.Multiple(() => + { + Assert.That(parsedProductVersion.Major, Is.EqualTo(expectedMajor)); + Assert.That(parsedProductVersion.Minor, Is.EqualTo(expectedMinor)); + Assert.That(parsedProductVersion.Build, Is.EqualTo(expectedBuild)); + Assert.That(parsedProductVersion.PrivatePart, Is.EqualTo(expectedRevision)); + }); + } + + [Test] + [TestCase("1-test.2.3.4", 1, 0, 0, 0)] + [TestCase("1-test5.2.3.4", 1, 0, 0, 0)] + [TestCase("1.2-test.3.4", 1, 2, 0, 0)] + [TestCase("1.2-test5.3.4", 1, 2, 0, 0)] + [TestCase("1.2.3-test.4", 1, 2, 3, 0)] + [TestCase("1.2.3-test5.4", 1, 2, 3, 0)] + [TestCase("1.2.3.4-test", 1, 2, 3, 4)] + [TestCase("1.2.3.4-test5", 1, 2, 3, 4)] + public void ProductVersionParser_Parse_ShouldSkipTheRestOfTheSegmentsWhenOneContainsMoreThanJustOneNumber( + string productVersion, + int expectedMajor, + int expectedMinor, + int expectedBuild, + int expectedRevision) + { + // Act + var parsedProductVersion = ProductVersionParser.Parse(productVersion); + + // Assert + Assert.Multiple(() => + { + Assert.That(parsedProductVersion.Major, Is.EqualTo(expectedMajor)); + Assert.That(parsedProductVersion.Minor, Is.EqualTo(expectedMinor)); + Assert.That(parsedProductVersion.Build, Is.EqualTo(expectedBuild)); + Assert.That(parsedProductVersion.PrivatePart, Is.EqualTo(expectedRevision)); + }); + } + + [Test] + [TestCase("", 0, 0, 0, 0)] + [TestCase("1", 1, 0, 0, 0)] + [TestCase("1.2", 1, 2, 0, 0)] + [TestCase("1.2.3", 1, 2, 3, 0)] + [TestCase("1.2.3.4", 1, 2, 3, 4)] + public void ProductVersionParser_Parse_ShouldParseEachProvidedSegment( + string productVersion, + int expectedMajor, + int expectedMinor, + int expectedBuild, + int expectedRevision) + { + // Act + var parsedProductVersion = ProductVersionParser.Parse(productVersion); + + // Assert + Assert.Multiple(() => + { + Assert.That(parsedProductVersion.Major, Is.EqualTo(expectedMajor)); + Assert.That(parsedProductVersion.Minor, Is.EqualTo(expectedMinor)); + Assert.That(parsedProductVersion.Build, Is.EqualTo(expectedBuild)); + Assert.That(parsedProductVersion.PrivatePart, Is.EqualTo(expectedRevision)); + }); + } + } +} diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/ApiParityTests.cs b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/ApiParityTests.cs index 706b06a2c..ec46effa8 100644 --- a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/ApiParityTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/ApiParityTests.cs @@ -24,6 +24,13 @@ public void FileInfo() => typeof(System.IO.Abstractions.FileInfoBase) ); + [Test] + public void FileVersionInfo() => + AssertParity( + typeof(System.Diagnostics.FileVersionInfo), + typeof(System.IO.Abstractions.FileVersionInfoBase) + ); + [Test] public void Directory() => AssertParity( @@ -72,7 +79,8 @@ static IEnumerable GetMembers(Type type) => type .Select(x => x.Replace("System.IO.FileInfo", "System.IO.Abstractions.IFileInfo")) .Select(x => x.Replace("System.IO.DirectoryInfo", "System.IO.Abstractions.IDirectoryInfo")) .Select(x => x.Replace("System.IO.DriveInfo", "System.IO.Abstractions.IDriveInfo")) - .Select(x => x.Replace("System.IO.WaitForChangedResult", "System.IO.Abstractions.IWaitForChangedResult")); + .Select(x => x.Replace("System.IO.WaitForChangedResult", "System.IO.Abstractions.IWaitForChangedResult")) + .Where(x => x != "System.Diagnostics.FileVersionInfo GetVersionInfo(System.String)"); var abstractionMembers = GetMembers(abstractionType) .Where(x => !x.Contains("op_Implicit")) .Where(x => x != "System.IO.Abstractions.IFileSystem get_FileSystem()") diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/FileVersionInfoBaseConversionTests.cs b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/FileVersionInfoBaseConversionTests.cs new file mode 100644 index 000000000..b53a64e9e --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/FileVersionInfoBaseConversionTests.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using System.Diagnostics; + +namespace System.IO.Abstractions.Tests +{ + /// + /// Unit tests for the conversion operators of the class. + /// + public class FileVersionInfoBaseConversionTests + { + /// + /// Tests that a null is correctly converted to a null without exception. + /// + [Test] + public void FileVersionInfoBase_FromFileVersionInfo_ShouldReturnNullIfFileVersionInfoIsNull() + { + // Arrange + FileVersionInfo fileVersionInfo = null; + + // Act + FileVersionInfoBase actual = fileVersionInfo; + + // Assert + Assert.That(actual, Is.Null); + } + } +} diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 6.0.snap b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 6.0.snap new file mode 100644 index 000000000..7cb3b9523 --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 6.0.snap @@ -0,0 +1,4 @@ +{ + "ExtraMembers": [], + "MissingMembers": [] +} diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 7.0.snap b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 7.0.snap new file mode 100644 index 000000000..7cb3b9523 --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 7.0.snap @@ -0,0 +1,4 @@ +{ + "ExtraMembers": [], + "MissingMembers": [] +} diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 8.0.snap b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 8.0.snap new file mode 100644 index 000000000..7cb3b9523 --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET 8.0.snap @@ -0,0 +1,4 @@ +{ + "ExtraMembers": [], + "MissingMembers": [] +} diff --git a/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET Framework 4.6.2.snap b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET Framework 4.6.2.snap new file mode 100644 index 000000000..7cb3b9523 --- /dev/null +++ b/tests/TestableIO.System.IO.Abstractions.Wrappers.Tests/__snapshots__/ApiParityTests.FileVersionInfo_.NET Framework 4.6.2.snap @@ -0,0 +1,4 @@ +{ + "ExtraMembers": [], + "MissingMembers": [] +} diff --git a/version.json b/version.json index 081fbf157..8e4c8f245 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "21.1", + "version": "21.2", "assemblyVersion": { "precision": "major" },