Skip to content

Commit e4c5bb5

Browse files
authored
feat: prepare simulating other file systems (#546)
* Allow simulating other file systems * Simulate `Path.IsPathRooted` * Fix or skip failing tests
1 parent 284f0cc commit e4c5bb5

File tree

11 files changed

+356
-32
lines changed

11 files changed

+356
-32
lines changed

Source/Testably.Abstractions.Testing/FileSystem/DriveInfoFactoryMock.cs

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public IDriveInfo[] GetDrives()
3030
return _fileSystem.Storage.GetDrives()
3131
.Where(x => !x.IsUncPath)
3232
.Cast<IDriveInfo>()
33+
.OrderBy(x => x.Name)
3334
.ToArray();
3435
}
3536

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.IO;
2+
#if FEATURE_SPAN
3+
using System;
4+
#endif
5+
6+
namespace Testably.Abstractions.Testing.Helpers;
7+
8+
internal partial class Execute
9+
{
10+
private class LinuxPath(MockFileSystem fileSystem) : NativePath(fileSystem)
11+
{
12+
#if FEATURE_SPAN
13+
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
14+
public override bool IsPathRooted(ReadOnlySpan<char> path)
15+
=> IsPathRooted(path.ToString());
16+
#endif
17+
18+
/// <inheritdoc cref="Path.IsPathRooted(string)" />
19+
public override bool IsPathRooted(string? path)
20+
=> path?.Length > 0 && path[0] == '/';
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#if FEATURE_SPAN
2+
#endif
3+
#if FEATURE_FILESYSTEM_NET7
4+
using Testably.Abstractions.Testing.Storage;
5+
#endif
6+
7+
namespace Testably.Abstractions.Testing.Helpers;
8+
9+
internal partial class Execute
10+
{
11+
private sealed class MacPath(MockFileSystem fileSystem) : LinuxPath(fileSystem);
12+
}

Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs

+12-19
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,8 @@ namespace Testably.Abstractions.Testing.Helpers;
1111

1212
internal partial class Execute
1313
{
14-
private sealed class NativePath : IPath
14+
private class NativePath(MockFileSystem fileSystem) : IPath
1515
{
16-
private readonly MockFileSystem _fileSystem;
17-
18-
public NativePath(MockFileSystem fileSystem)
19-
{
20-
_fileSystem = fileSystem;
21-
}
22-
2316
#region IPath Members
2417

2518
/// <inheritdoc cref="Path.AltDirectorySeparatorChar" />
@@ -31,7 +24,7 @@ public char DirectorySeparatorChar
3124
=> System.IO.Path.DirectorySeparatorChar;
3225

3326
/// <inheritdoc cref="IFileSystemEntity.FileSystem" />
34-
public IFileSystem FileSystem => _fileSystem;
27+
public IFileSystem FileSystem => fileSystem;
3528

3629
/// <inheritdoc cref="Path.PathSeparator" />
3730
public char PathSeparator
@@ -83,7 +76,7 @@ public bool Exists([NotNullWhen(true)] string? path)
8376
return false;
8477
}
8578

86-
return _fileSystem.Storage.GetContainer(_fileSystem.Storage.GetLocation(path))
79+
return fileSystem.Storage.GetContainer(fileSystem.Storage.GetLocation(path))
8780
is not NullContainer;
8881
}
8982
#endif
@@ -134,11 +127,11 @@ public ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
134127
/// <inheritdoc cref="Path.GetFullPath(string)" />
135128
public string GetFullPath(string path)
136129
{
137-
path.EnsureValidArgument(_fileSystem, nameof(path));
130+
path.EnsureValidArgument(fileSystem, nameof(path));
138131

139132
string? pathRoot = System.IO.Path.GetPathRoot(path);
140133
string? directoryRoot =
141-
System.IO.Path.GetPathRoot(_fileSystem.Storage.CurrentDirectory);
134+
System.IO.Path.GetPathRoot(fileSystem.Storage.CurrentDirectory);
142135
if (!string.IsNullOrEmpty(pathRoot) && !string.IsNullOrEmpty(directoryRoot))
143136
{
144137
if (char.ToUpperInvariant(pathRoot[0]) != char.ToUpperInvariant(directoryRoot[0]))
@@ -153,7 +146,7 @@ public string GetFullPath(string path)
153146
}
154147

155148
return System.IO.Path.GetFullPath(System.IO.Path.Combine(
156-
_fileSystem.Storage.CurrentDirectory,
149+
fileSystem.Storage.CurrentDirectory,
157150
path));
158151
}
159152

@@ -189,11 +182,11 @@ public string GetRandomFileName()
189182
/// <inheritdoc cref="Path.GetRelativePath(string, string)" />
190183
public string GetRelativePath(string relativeTo, string path)
191184
{
192-
relativeTo.EnsureValidArgument(_fileSystem, nameof(relativeTo));
193-
path.EnsureValidArgument(_fileSystem, nameof(path));
185+
relativeTo.EnsureValidArgument(fileSystem, nameof(relativeTo));
186+
path.EnsureValidArgument(fileSystem, nameof(path));
194187

195-
relativeTo = _fileSystem.Execute.Path.GetFullPath(relativeTo);
196-
path = _fileSystem.Execute.Path.GetFullPath(path);
188+
relativeTo = fileSystem.Execute.Path.GetFullPath(relativeTo);
189+
path = fileSystem.Execute.Path.GetFullPath(path);
197190

198191
return System.IO.Path.GetRelativePath(relativeTo, path);
199192
}
@@ -235,12 +228,12 @@ public bool IsPathFullyQualified(string path)
235228

236229
#if FEATURE_SPAN
237230
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
238-
public bool IsPathRooted(ReadOnlySpan<char> path)
231+
public virtual bool IsPathRooted(ReadOnlySpan<char> path)
239232
=> System.IO.Path.IsPathRooted(path);
240233
#endif
241234

242235
/// <inheritdoc cref="Path.IsPathRooted(string)" />
243-
public bool IsPathRooted(string? path)
236+
public virtual bool IsPathRooted(string? path)
244237
=> System.IO.Path.IsPathRooted(path);
245238

246239
#if FEATURE_PATH_JOIN
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.IO;
2+
#if FEATURE_SPAN
3+
using System;
4+
#endif
5+
6+
namespace Testably.Abstractions.Testing.Helpers;
7+
8+
internal partial class Execute
9+
{
10+
private sealed class WindowsPath(MockFileSystem fileSystem) : NativePath(fileSystem)
11+
{
12+
#if FEATURE_SPAN
13+
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
14+
public override bool IsPathRooted(ReadOnlySpan<char> path)
15+
=> IsPathRooted(path.ToString());
16+
#endif
17+
18+
/// <inheritdoc cref="Path.IsPathRooted(string)" />
19+
public override bool IsPathRooted(string? path)
20+
{
21+
int? length = path?.Length;
22+
return (length >= 1 && IsDirectorySeparator(path![0])) ||
23+
(length >= 2 && IsValidDriveChar(path![0]) && path[1] == VolumeSeparatorChar);
24+
}
25+
26+
/// <summary>
27+
/// True if the given character is a directory separator.
28+
/// </summary>
29+
/// <remarks>https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L280</remarks>
30+
private static bool IsDirectorySeparator(char c)
31+
=> c == '\\' || c == '/';
32+
33+
/// <summary>
34+
/// Returns true if the given character is a valid drive letter
35+
/// </summary>
36+
/// <remarks>https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L72</remarks>
37+
private static bool IsValidDriveChar(char value)
38+
=> (uint)((value | 0x20) - 'a') <= 'z' - 'a';
39+
}
40+
}

Source/Testably.Abstractions.Testing/Helpers/Execute.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,23 @@ internal Execute(MockFileSystem fileSystem, OSPlatform osPlatform, bool isNetFra
4747
StringComparisonMode = IsLinux
4848
? StringComparison.Ordinal
4949
: StringComparison.OrdinalIgnoreCase;
50-
Path = new NativePath(fileSystem);
50+
if (IsLinux)
51+
{
52+
Path = new LinuxPath(fileSystem);
53+
}
54+
else if (IsMac)
55+
{
56+
Path = new MacPath(fileSystem);
57+
}
58+
else if (IsWindows)
59+
{
60+
Path = new WindowsPath(fileSystem);
61+
}
62+
else
63+
{
64+
throw new NotSupportedException(
65+
"The operating system must be one of Linux, OSX or Windows");
66+
}
5167
}
5268

5369
internal Execute(MockFileSystem fileSystem)

Source/Testably.Abstractions.Testing/MockFileSystem.cs

+79-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System;
33
using System.Collections.Generic;
44
using System.IO;
5+
using System.Runtime.InteropServices;
56
using Testably.Abstractions.Testing.FileSystem;
67
using Testably.Abstractions.Testing.Helpers;
78
using Testably.Abstractions.Testing.Statistics;
@@ -82,9 +83,19 @@ internal IReadOnlyList<IStorageContainer> StorageContainers
8283
/// <summary>
8384
/// Initializes the <see cref="MockFileSystem" />.
8485
/// </summary>
85-
public MockFileSystem()
86+
public MockFileSystem() : this(_ => { }) { }
87+
88+
/// <summary>
89+
/// Initializes the <see cref="MockFileSystem" /> with the <paramref name="initializationCallback" />.
90+
/// </summary>
91+
internal MockFileSystem(Action<Initialization> initializationCallback)
8692
{
87-
Execute = new Execute(this);
93+
Initialization initialization = new();
94+
initializationCallback(initialization);
95+
96+
Execute = initialization.OperatingSystem == null
97+
? new Execute(this)
98+
: new Execute(this, initialization.OperatingSystem.Value);
8899
StatisticsRegistration = new FileSystemStatistics(this);
89100
using IDisposable release = StatisticsRegistration.Ignore();
90101
RandomSystem = new MockRandomSystem();
@@ -101,7 +112,7 @@ public MockFileSystem()
101112
FileSystemWatcher = new FileSystemWatcherFactoryMock(this);
102113
SafeFileHandleStrategy = new NullSafeFileHandleStrategy();
103114
AccessControlStrategy = new NullAccessControlStrategy();
104-
AddDriveFromCurrentDirectory();
115+
InitializeFileSystem(initialization);
105116
}
106117

107118
#region IFileSystem Members
@@ -181,11 +192,17 @@ public MockFileSystem WithSafeFileHandleStrategy(
181192
return this;
182193
}
183194

184-
private void AddDriveFromCurrentDirectory()
195+
private void InitializeFileSystem(Initialization initialization)
185196
{
186197
try
187198
{
188-
string? root = Path.GetPathRoot(System.IO.Directory.GetCurrentDirectory());
199+
if (initialization.CurrentDirectory != null)
200+
{
201+
IDirectoryInfo directoryInfo = DirectoryInfo.New(initialization.CurrentDirectory);
202+
Storage.CurrentDirectory = directoryInfo.FullName;
203+
}
204+
205+
string? root = Execute.Path.GetPathRoot(Directory.GetCurrentDirectory());
189206
if (root != null &&
190207
root[0] != _storage.MainDrive.Name[0])
191208
{
@@ -198,4 +215,61 @@ private void AddDriveFromCurrentDirectory()
198215
// due to brittle tests on MacOS
199216
}
200217
}
218+
219+
/// <summary>
220+
/// The initialization options for the <see cref="MockFileSystem" />.
221+
/// </summary>
222+
internal class Initialization
223+
{
224+
/// <summary>
225+
/// The current directory.
226+
/// </summary>
227+
internal string? CurrentDirectory { get; private set; }
228+
229+
/// <summary>
230+
/// The simulated operating system.
231+
/// </summary>
232+
internal OSPlatform? OperatingSystem { get; private set; }
233+
234+
/// <summary>
235+
/// Specify the operating system that should be simulated.
236+
/// </summary>
237+
/// <remarks>
238+
/// Supported values are<br />
239+
/// - <see cref="OSPlatform.Linux" /><br />
240+
/// - <see cref="OSPlatform.OSX" /><br />
241+
/// - <see cref="OSPlatform.Windows" />
242+
/// </remarks>
243+
internal Initialization SimulatingOperatingSystem(OSPlatform operatingSystem)
244+
{
245+
if (operatingSystem != OSPlatform.Linux &&
246+
operatingSystem != OSPlatform.OSX &&
247+
operatingSystem != OSPlatform.Windows)
248+
{
249+
throw new NotSupportedException(
250+
"Only Linux, OSX and Windows are supported operating systems.");
251+
}
252+
253+
OperatingSystem = operatingSystem;
254+
return this;
255+
}
256+
257+
/// <summary>
258+
/// Use the provided <paramref name="path" /> as current directory.
259+
/// </summary>
260+
internal Initialization UseCurrentDirectory(string path)
261+
{
262+
CurrentDirectory = path;
263+
return this;
264+
}
265+
266+
/// <summary>
267+
/// Use <see cref="Directory.GetCurrentDirectory()" /> as current directory.
268+
/// </summary>
269+
internal Initialization UseCurrentDirectory()
270+
{
271+
CurrentDirectory = System.IO.Directory.GetCurrentDirectory();
272+
return this;
273+
}
274+
}
201275
}

Tests/Testably.Abstractions.Testing.Tests/Helpers/ExecuteTests.cs

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ namespace Testably.Abstractions.Testing.Tests.Helpers;
55

66
public sealed class ExecuteTests
77
{
8+
#if !NET48
9+
[Fact]
10+
public void Constructor_ForFreeBSD_ShouldThrowNotSupportedException()
11+
{
12+
Exception? exception = Record.Exception(() =>
13+
{
14+
_ = new Execute(new MockFileSystem(), OSPlatform.FreeBSD);
15+
});
16+
17+
exception.Should().BeOfType<NotSupportedException>()
18+
.Which.Message.Should()
19+
.Contain("Linux").And
20+
.Contain("Windows").And
21+
.Contain("OSX");
22+
}
23+
#endif
24+
825
[Fact]
926
public void Constructor_ForLinux_ShouldInitializeAccordingly()
1027
{

0 commit comments

Comments
 (0)