Skip to content

Commit ce67de2

Browse files
authored
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.
1 parent 7bcc722 commit ce67de2

26 files changed

+1063
-11
lines changed

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileData.cs

+5
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ public MockFileData(MockFileData template)
119119
/// </summary>
120120
public byte[] Contents { get; set; }
121121

122+
/// <summary>
123+
/// Gets or sets the file version info of the <see cref="MockFileData"/>
124+
/// </summary>
125+
public IFileVersionInfo FileVersionInfo { get; set; }
126+
122127
/// <summary>
123128
/// Gets or sets the string contents of the <see cref="MockFileData"/>.
124129
/// </summary>

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileInfo.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public override void Encrypt()
259259
var mockFileData = GetMockFileDataForWrite();
260260
mockFileData.Attributes |= FileAttributes.Encrypted;
261261
}
262-
262+
263263
/// <inheritdoc />
264264
public override void MoveTo(string destFileName)
265265
{
@@ -323,7 +323,7 @@ public override IFileInfo Replace(string destinationFileName, string destination
323323
mockFile.Replace(path, destinationFileName, destinationBackupFileName, ignoreMetadataErrors);
324324
return mockFileSystem.FileInfo.New(destinationFileName);
325325
}
326-
326+
327327
/// <inheritdoc />
328328
public override IDirectoryInfo Directory
329329
{

src/TestableIO.System.IO.Abstractions.TestingHelpers/MockFileSystem.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public MockFileSystem(IDictionary<string, MockFileData> files, MockFileSystemOpt
6565
File = new MockFile(this);
6666
Directory = new MockDirectory(this, currentDirectory);
6767
FileInfo = new MockFileInfoFactory(this);
68+
FileVersionInfo = new MockFileVersionInfoFactory(this);
6869
FileStream = new MockFileStreamFactory(this);
6970
DirectoryInfo = new MockDirectoryInfoFactory(this);
7071
DriveInfo = new MockDriveInfoFactory(this);
@@ -98,6 +99,8 @@ public MockFileSystem(IDictionary<string, MockFileData> files, MockFileSystemOpt
9899
/// <inheritdoc />
99100
public override IFileInfoFactory FileInfo { get; }
100101
/// <inheritdoc />
102+
public override IFileVersionInfoFactory FileVersionInfo { get; }
103+
/// <inheritdoc />
101104
public override IFileStreamFactory FileStream { get; }
102105
/// <inheritdoc />
103106
public override IPath Path { get; }
@@ -252,6 +255,8 @@ public void AddFile(string path, MockFileData mockFile)
252255
AddDirectory(directoryPath);
253256
}
254257

258+
mockFile.FileVersionInfo ??= new MockFileVersionInfo(fixedPath);
259+
255260
SetEntry(fixedPath, mockFile);
256261
}
257262

@@ -568,7 +573,7 @@ private bool FileIsReadOnly(string path)
568573
}
569574

570575
#if FEATURE_SERIALIZABLE
571-
[Serializable]
576+
[Serializable]
572577
#endif
573578
private class FileSystemEntry
574579
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
4+
namespace System.IO.Abstractions.TestingHelpers
5+
{
6+
/// <inheritdoc />
7+
#if FEATURE_SERIALIZABLE
8+
[Serializable]
9+
#endif
10+
public class MockFileVersionInfo : FileVersionInfoBase
11+
{
12+
/// <inheritdoc />
13+
public MockFileVersionInfo(
14+
string fileName,
15+
string fileVersion = null,
16+
string productVersion = null,
17+
string fileDescription = null,
18+
string productName = null,
19+
string companyName = null,
20+
string comments = null,
21+
string internalName = null,
22+
bool isDebug = false,
23+
bool isPatched = false,
24+
bool isPrivateBuild = false,
25+
bool isPreRelease = false,
26+
bool isSpecialBuild = false,
27+
string language = null,
28+
string legalCopyright = null,
29+
string legalTrademarks = null,
30+
string originalFilename = null,
31+
string privateBuild = null,
32+
string specialBuild = null)
33+
{
34+
FileName = fileName;
35+
FileVersion = fileVersion;
36+
ProductVersion = productVersion;
37+
FileDescription = fileDescription;
38+
ProductName = productName;
39+
CompanyName = companyName;
40+
Comments = comments;
41+
InternalName = internalName;
42+
IsDebug = isDebug;
43+
IsPatched = isPatched;
44+
IsPrivateBuild = isPrivateBuild;
45+
IsPreRelease = isPreRelease;
46+
IsSpecialBuild = isSpecialBuild;
47+
Language = language;
48+
LegalCopyright = legalCopyright;
49+
LegalTrademarks = legalTrademarks;
50+
OriginalFilename = originalFilename;
51+
PrivateBuild = privateBuild;
52+
SpecialBuild = specialBuild;
53+
54+
if (Version.TryParse(fileVersion, out Version version))
55+
{
56+
FileMajorPart = version.Major;
57+
FileMinorPart = version.Minor;
58+
FileBuildPart = version.Build;
59+
FilePrivatePart = version.Revision;
60+
}
61+
62+
var parsedProductVersion = ProductVersionParser.Parse(productVersion);
63+
64+
ProductMajorPart = parsedProductVersion.Major;
65+
ProductMinorPart = parsedProductVersion.Minor;
66+
ProductBuildPart = parsedProductVersion.Build;
67+
ProductPrivatePart = parsedProductVersion.PrivatePart;
68+
}
69+
70+
/// <inheritdoc/>
71+
public override string FileName { get; }
72+
73+
/// <inheritdoc/>
74+
public override string FileVersion { get; }
75+
76+
/// <inheritdoc/>
77+
public override string ProductVersion { get; }
78+
79+
/// <inheritdoc/>
80+
public override string FileDescription { get; }
81+
82+
/// <inheritdoc/>
83+
public override string ProductName { get; }
84+
85+
/// <inheritdoc/>
86+
public override string CompanyName { get; }
87+
88+
/// <inheritdoc/>
89+
public override string Comments { get; }
90+
91+
/// <inheritdoc/>
92+
public override string InternalName { get; }
93+
94+
/// <inheritdoc/>
95+
public override bool IsDebug { get; }
96+
97+
/// <inheritdoc/>
98+
public override bool IsPatched { get; }
99+
100+
/// <inheritdoc/>
101+
public override bool IsPrivateBuild { get; }
102+
103+
/// <inheritdoc/>
104+
public override bool IsPreRelease { get; }
105+
106+
/// <inheritdoc/>
107+
public override bool IsSpecialBuild { get; }
108+
109+
/// <inheritdoc/>
110+
public override string Language { get; }
111+
112+
/// <inheritdoc/>
113+
public override string LegalCopyright { get; }
114+
115+
/// <inheritdoc/>
116+
public override string LegalTrademarks { get; }
117+
118+
/// <inheritdoc/>
119+
public override string OriginalFilename { get; }
120+
121+
/// <inheritdoc/>
122+
public override string PrivateBuild { get; }
123+
124+
/// <inheritdoc/>
125+
public override string SpecialBuild { get; }
126+
127+
/// <inheritdoc/>
128+
public override int FileMajorPart { get; }
129+
130+
/// <inheritdoc/>
131+
public override int FileMinorPart { get; }
132+
133+
/// <inheritdoc/>
134+
public override int FileBuildPart { get; }
135+
136+
/// <inheritdoc/>
137+
public override int FilePrivatePart { get; }
138+
139+
/// <inheritdoc/>
140+
public override int ProductMajorPart { get; }
141+
142+
/// <inheritdoc/>
143+
public override int ProductMinorPart { get; }
144+
145+
/// <inheritdoc/>
146+
public override int ProductBuildPart { get; }
147+
148+
/// <inheritdoc/>
149+
public override int ProductPrivatePart { get; }
150+
151+
/// <inheritdoc cref="FileVersionInfo.ToString" />
152+
public override string ToString()
153+
{
154+
// An initial capacity of 512 was chosen because it is large enough to cover
155+
// the size of the static strings with enough capacity left over to cover
156+
// average length property values.
157+
var sb = new StringBuilder(512);
158+
sb.Append("File: ").AppendLine(FileName);
159+
sb.Append("InternalName: ").AppendLine(InternalName);
160+
sb.Append("OriginalFilename: ").AppendLine(OriginalFilename);
161+
sb.Append("FileVersion: ").AppendLine(FileVersion);
162+
sb.Append("FileDescription: ").AppendLine(FileDescription);
163+
sb.Append("Product: ").AppendLine(ProductName);
164+
sb.Append("ProductVersion: ").AppendLine(ProductVersion);
165+
sb.Append("Debug: ").AppendLine(IsDebug.ToString());
166+
sb.Append("Patched: ").AppendLine(IsPatched.ToString());
167+
sb.Append("PreRelease: ").AppendLine(IsPreRelease.ToString());
168+
sb.Append("PrivateBuild: ").AppendLine(IsPrivateBuild.ToString());
169+
sb.Append("SpecialBuild: ").AppendLine(IsSpecialBuild.ToString());
170+
sb.Append("Language: ").AppendLine(Language);
171+
return sb.ToString();
172+
}
173+
}
174+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace System.IO.Abstractions.TestingHelpers
2+
{
3+
/// <inheritdoc />
4+
#if FEATURE_SERIALIZABLE
5+
[Serializable]
6+
#endif
7+
public class MockFileVersionInfoFactory : IFileVersionInfoFactory
8+
{
9+
private readonly IMockFileDataAccessor mockFileSystem;
10+
11+
/// <inheritdoc />
12+
public MockFileVersionInfoFactory(IMockFileDataAccessor mockFileSystem)
13+
{
14+
this.mockFileSystem = mockFileSystem ?? throw new ArgumentNullException(nameof(mockFileSystem));
15+
}
16+
17+
/// <inheritdoc />
18+
public IFileSystem FileSystem => mockFileSystem;
19+
20+
/// <inheritdoc />
21+
public IFileVersionInfo GetVersionInfo(string fileName)
22+
{
23+
MockFileData mockFileData = mockFileSystem.GetFile(fileName);
24+
25+
if (mockFileData != null)
26+
{
27+
return mockFileData.FileVersionInfo;
28+
}
29+
30+
throw CommonExceptions.FileNotFound(fileName);
31+
}
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.Reflection;
2+
using System.Text.RegularExpressions;
3+
4+
namespace System.IO.Abstractions.TestingHelpers
5+
{
6+
/// <summary>
7+
/// Provides functionality to parse a product version string into its major, minor, build, and private parts.
8+
/// </summary>
9+
internal static class ProductVersionParser
10+
{
11+
/// <summary>
12+
/// Parses a product version string and extracts the numeric values for the major, minor, build, and private parts,
13+
/// mimicking the behavior of the <see cref="AssemblyInformationalVersionAttribute"/> attribute.
14+
/// </summary>
15+
/// <param name="productVersion">The product version string to parse.</param>
16+
/// <returns>
17+
/// A <see cref="ProductVersion"/> object containing the parsed major, minor, build, and private parts.
18+
/// If the input is invalid, returns a <see cref="ProductVersion"/> with all parts set to 0.
19+
/// </returns>
20+
/// <remarks>
21+
/// The method splits the input string into segments separated by dots ('.') and attempts to extract
22+
/// the leading numeric value from each segment. A maximum of 4 segments are processed; if more than
23+
/// 4 segments are present, all segments are ignored. Additionally, if a segment does not contain
24+
/// a valid numeric part at its start or it contains more than just a number, the rest of the segments are ignored.
25+
/// </remarks>
26+
public static ProductVersion Parse(string productVersion)
27+
{
28+
if (string.IsNullOrWhiteSpace(productVersion))
29+
{
30+
return new();
31+
}
32+
33+
var segments = productVersion.Split('.');
34+
if (segments.Length > 4)
35+
{
36+
// if more than 4 segments are present, all segments are ignored
37+
return new();
38+
}
39+
40+
var regex = new Regex(@"^\d+");
41+
42+
int[] parts = new int[4];
43+
44+
for (int i = 0; i < segments.Length; i++)
45+
{
46+
var match = regex.Match(segments[i]);
47+
if (match.Success && int.TryParse(match.Value, out int number))
48+
{
49+
parts[i] = number;
50+
51+
if (match.Value != segments[i])
52+
{
53+
// when a segment contains more than a number, the rest of the segments are ignored
54+
break;
55+
}
56+
}
57+
else
58+
{
59+
// when a segment is not valid, the rest of the segments are ignored
60+
break;
61+
}
62+
}
63+
64+
return new()
65+
{
66+
Major = parts[0],
67+
Minor = parts[1],
68+
Build = parts[2],
69+
PrivatePart = parts[3]
70+
};
71+
}
72+
73+
/// <summary>
74+
/// Represents a product version with numeric parts for major, minor, build, and private versions.
75+
/// </summary>
76+
public class ProductVersion
77+
{
78+
/// <summary>
79+
/// Gets the major part of the version number
80+
/// </summary>
81+
public int Major { get; init; }
82+
83+
/// <summary>
84+
/// Gets the minor part of the version number
85+
/// </summary>
86+
public int Minor { get; init; }
87+
88+
/// <summary>
89+
/// Gets the build part of the version number
90+
/// </summary>
91+
public int Build { get; init; }
92+
93+
/// <summary>
94+
/// Gets the private part of the version number
95+
/// </summary>
96+
public int PrivatePart { get; init; }
97+
}
98+
}
99+
}

src/TestableIO.System.IO.Abstractions.Wrappers/FileInfoBase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal FileInfoBase() { }
3636

3737
/// <inheritdoc cref="IFileInfo.Encrypt"/>
3838
public abstract void Encrypt();
39-
39+
4040
/// <inheritdoc cref="IFileInfo.MoveTo(string)"/>
4141
public abstract void MoveTo(string destFileName);
4242

@@ -73,7 +73,7 @@ internal FileInfoBase() { }
7373

7474
/// <inheritdoc cref="IFileInfo.Replace(string,string,bool)"/>
7575
public abstract IFileInfo Replace(string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors);
76-
76+
7777
/// <inheritdoc cref="IFileInfo.Directory"/>
7878
public abstract IDirectoryInfo Directory { get; }
7979

0 commit comments

Comments
 (0)