Skip to content

Commit

Permalink
feat: prepare simulating other file systems (#546)
Browse files Browse the repository at this point in the history
* Allow simulating other file systems

* Simulate `Path.IsPathRooted`

* Fix or skip failing tests
  • Loading branch information
vbreuss authored Apr 6, 2024
1 parent 284f0cc commit e4c5bb5
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public IDriveInfo[] GetDrives()
return _fileSystem.Storage.GetDrives()
.Where(x => !x.IsUncPath)
.Cast<IDriveInfo>()
.OrderBy(x => x.Name)
.ToArray();
}

Expand Down
22 changes: 22 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.LinuxPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.IO;
#if FEATURE_SPAN
using System;
#endif

namespace Testably.Abstractions.Testing.Helpers;

internal partial class Execute
{
private class LinuxPath(MockFileSystem fileSystem) : NativePath(fileSystem)
{
#if FEATURE_SPAN
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
public override bool IsPathRooted(ReadOnlySpan<char> path)
=> IsPathRooted(path.ToString());
#endif

/// <inheritdoc cref="Path.IsPathRooted(string)" />
public override bool IsPathRooted(string? path)
=> path?.Length > 0 && path[0] == '/';
}
}
12 changes: 12 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.MacPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if FEATURE_SPAN
#endif
#if FEATURE_FILESYSTEM_NET7
using Testably.Abstractions.Testing.Storage;
#endif

namespace Testably.Abstractions.Testing.Helpers;

internal partial class Execute
{
private sealed class MacPath(MockFileSystem fileSystem) : LinuxPath(fileSystem);
}
31 changes: 12 additions & 19 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,8 @@ namespace Testably.Abstractions.Testing.Helpers;

internal partial class Execute
{
private sealed class NativePath : IPath
private class NativePath(MockFileSystem fileSystem) : IPath
{
private readonly MockFileSystem _fileSystem;

public NativePath(MockFileSystem fileSystem)
{
_fileSystem = fileSystem;
}

#region IPath Members

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

/// <inheritdoc cref="IFileSystemEntity.FileSystem" />
public IFileSystem FileSystem => _fileSystem;
public IFileSystem FileSystem => fileSystem;

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

return _fileSystem.Storage.GetContainer(_fileSystem.Storage.GetLocation(path))
return fileSystem.Storage.GetContainer(fileSystem.Storage.GetLocation(path))
is not NullContainer;
}
#endif
Expand Down Expand Up @@ -134,11 +127,11 @@ public ReadOnlySpan<char> GetFileNameWithoutExtension(ReadOnlySpan<char> path)
/// <inheritdoc cref="Path.GetFullPath(string)" />
public string GetFullPath(string path)
{
path.EnsureValidArgument(_fileSystem, nameof(path));
path.EnsureValidArgument(fileSystem, nameof(path));

string? pathRoot = System.IO.Path.GetPathRoot(path);
string? directoryRoot =
System.IO.Path.GetPathRoot(_fileSystem.Storage.CurrentDirectory);
System.IO.Path.GetPathRoot(fileSystem.Storage.CurrentDirectory);
if (!string.IsNullOrEmpty(pathRoot) && !string.IsNullOrEmpty(directoryRoot))
{
if (char.ToUpperInvariant(pathRoot[0]) != char.ToUpperInvariant(directoryRoot[0]))
Expand All @@ -153,7 +146,7 @@ public string GetFullPath(string path)
}

return System.IO.Path.GetFullPath(System.IO.Path.Combine(
_fileSystem.Storage.CurrentDirectory,
fileSystem.Storage.CurrentDirectory,
path));
}

Expand Down Expand Up @@ -189,11 +182,11 @@ public string GetRandomFileName()
/// <inheritdoc cref="Path.GetRelativePath(string, string)" />
public string GetRelativePath(string relativeTo, string path)
{
relativeTo.EnsureValidArgument(_fileSystem, nameof(relativeTo));
path.EnsureValidArgument(_fileSystem, nameof(path));
relativeTo.EnsureValidArgument(fileSystem, nameof(relativeTo));
path.EnsureValidArgument(fileSystem, nameof(path));

relativeTo = _fileSystem.Execute.Path.GetFullPath(relativeTo);
path = _fileSystem.Execute.Path.GetFullPath(path);
relativeTo = fileSystem.Execute.Path.GetFullPath(relativeTo);
path = fileSystem.Execute.Path.GetFullPath(path);

return System.IO.Path.GetRelativePath(relativeTo, path);
}
Expand Down Expand Up @@ -235,12 +228,12 @@ public bool IsPathFullyQualified(string path)

#if FEATURE_SPAN
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
public bool IsPathRooted(ReadOnlySpan<char> path)
public virtual bool IsPathRooted(ReadOnlySpan<char> path)
=> System.IO.Path.IsPathRooted(path);
#endif

/// <inheritdoc cref="Path.IsPathRooted(string)" />
public bool IsPathRooted(string? path)
public virtual bool IsPathRooted(string? path)
=> System.IO.Path.IsPathRooted(path);

#if FEATURE_PATH_JOIN
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.IO;
#if FEATURE_SPAN
using System;
#endif

namespace Testably.Abstractions.Testing.Helpers;

internal partial class Execute
{
private sealed class WindowsPath(MockFileSystem fileSystem) : NativePath(fileSystem)
{
#if FEATURE_SPAN
/// <inheritdoc cref="Path.IsPathRooted(ReadOnlySpan{char})" />
public override bool IsPathRooted(ReadOnlySpan<char> path)
=> IsPathRooted(path.ToString());
#endif

/// <inheritdoc cref="Path.IsPathRooted(string)" />
public override bool IsPathRooted(string? path)
{
int? length = path?.Length;
return (length >= 1 && IsDirectorySeparator(path![0])) ||
(length >= 2 && IsValidDriveChar(path![0]) && path[1] == VolumeSeparatorChar);
}

/// <summary>
/// True if the given character is a directory separator.
/// </summary>
/// <remarks>https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L280</remarks>
private static bool IsDirectorySeparator(char c)
=> c == '\\' || c == '/';

/// <summary>
/// Returns true if the given character is a valid drive letter
/// </summary>
/// <remarks>https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L72</remarks>
private static bool IsValidDriveChar(char value)
=> (uint)((value | 0x20) - 'a') <= 'z' - 'a';
}
}
18 changes: 17 additions & 1 deletion Source/Testably.Abstractions.Testing/Helpers/Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,23 @@ internal Execute(MockFileSystem fileSystem, OSPlatform osPlatform, bool isNetFra
StringComparisonMode = IsLinux
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
Path = new NativePath(fileSystem);
if (IsLinux)
{
Path = new LinuxPath(fileSystem);
}
else if (IsMac)
{
Path = new MacPath(fileSystem);
}
else if (IsWindows)
{
Path = new WindowsPath(fileSystem);
}
else
{
throw new NotSupportedException(
"The operating system must be one of Linux, OSX or Windows");
}
}

internal Execute(MockFileSystem fileSystem)
Expand Down
84 changes: 79 additions & 5 deletions Source/Testably.Abstractions.Testing/MockFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Testably.Abstractions.Testing.FileSystem;
using Testably.Abstractions.Testing.Helpers;
using Testably.Abstractions.Testing.Statistics;
Expand Down Expand Up @@ -82,9 +83,19 @@ internal IReadOnlyList<IStorageContainer> StorageContainers
/// <summary>
/// Initializes the <see cref="MockFileSystem" />.
/// </summary>
public MockFileSystem()
public MockFileSystem() : this(_ => { }) { }

/// <summary>
/// Initializes the <see cref="MockFileSystem" /> with the <paramref name="initializationCallback" />.
/// </summary>
internal MockFileSystem(Action<Initialization> initializationCallback)
{
Execute = new Execute(this);
Initialization initialization = new();
initializationCallback(initialization);

Execute = initialization.OperatingSystem == null
? new Execute(this)
: new Execute(this, initialization.OperatingSystem.Value);
StatisticsRegistration = new FileSystemStatistics(this);
using IDisposable release = StatisticsRegistration.Ignore();
RandomSystem = new MockRandomSystem();
Expand All @@ -101,7 +112,7 @@ public MockFileSystem()
FileSystemWatcher = new FileSystemWatcherFactoryMock(this);
SafeFileHandleStrategy = new NullSafeFileHandleStrategy();
AccessControlStrategy = new NullAccessControlStrategy();
AddDriveFromCurrentDirectory();
InitializeFileSystem(initialization);
}

#region IFileSystem Members
Expand Down Expand Up @@ -181,11 +192,17 @@ public MockFileSystem WithSafeFileHandleStrategy(
return this;
}

private void AddDriveFromCurrentDirectory()
private void InitializeFileSystem(Initialization initialization)
{
try
{
string? root = Path.GetPathRoot(System.IO.Directory.GetCurrentDirectory());
if (initialization.CurrentDirectory != null)
{
IDirectoryInfo directoryInfo = DirectoryInfo.New(initialization.CurrentDirectory);
Storage.CurrentDirectory = directoryInfo.FullName;
}

string? root = Execute.Path.GetPathRoot(Directory.GetCurrentDirectory());
if (root != null &&
root[0] != _storage.MainDrive.Name[0])
{
Expand All @@ -198,4 +215,61 @@ private void AddDriveFromCurrentDirectory()
// due to brittle tests on MacOS
}
}

/// <summary>
/// The initialization options for the <see cref="MockFileSystem" />.
/// </summary>
internal class Initialization
{
/// <summary>
/// The current directory.
/// </summary>
internal string? CurrentDirectory { get; private set; }

/// <summary>
/// The simulated operating system.
/// </summary>
internal OSPlatform? OperatingSystem { get; private set; }

/// <summary>
/// Specify the operating system that should be simulated.
/// </summary>
/// <remarks>
/// Supported values are<br />
/// - <see cref="OSPlatform.Linux" /><br />
/// - <see cref="OSPlatform.OSX" /><br />
/// - <see cref="OSPlatform.Windows" />
/// </remarks>
internal Initialization SimulatingOperatingSystem(OSPlatform operatingSystem)
{
if (operatingSystem != OSPlatform.Linux &&
operatingSystem != OSPlatform.OSX &&
operatingSystem != OSPlatform.Windows)
{
throw new NotSupportedException(
"Only Linux, OSX and Windows are supported operating systems.");
}

OperatingSystem = operatingSystem;
return this;
}

/// <summary>
/// Use the provided <paramref name="path" /> as current directory.
/// </summary>
internal Initialization UseCurrentDirectory(string path)
{
CurrentDirectory = path;
return this;
}

/// <summary>
/// Use <see cref="Directory.GetCurrentDirectory()" /> as current directory.
/// </summary>
internal Initialization UseCurrentDirectory()
{
CurrentDirectory = System.IO.Directory.GetCurrentDirectory();
return this;
}
}
}
17 changes: 17 additions & 0 deletions Tests/Testably.Abstractions.Testing.Tests/Helpers/ExecuteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ namespace Testably.Abstractions.Testing.Tests.Helpers;

public sealed class ExecuteTests
{
#if !NET48
[Fact]
public void Constructor_ForFreeBSD_ShouldThrowNotSupportedException()
{
Exception? exception = Record.Exception(() =>
{
_ = new Execute(new MockFileSystem(), OSPlatform.FreeBSD);
});

exception.Should().BeOfType<NotSupportedException>()
.Which.Message.Should()
.Contain("Linux").And
.Contain("Windows").And
.Contain("OSX");
}
#endif

[Fact]
public void Constructor_ForLinux_ShouldInitializeAccordingly()
{
Expand Down
Loading

0 comments on commit e4c5bb5

Please sign in to comment.