Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prepare simulating other file systems #546

Merged
merged 6 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading