Skip to content

Commit

Permalink
feat: allow simulating other operating systems (#576)
Browse files Browse the repository at this point in the history
* Enable simulated tests for all file system methods

* Make simulation functionality public and throw a NotSupportedException on .NET Framework

* Skip simulated tests for AccessControl, as this feature is only supported on Windows.

* Disable obsolete warnings in tests

* Mention the simulation functionality in README.md
  • Loading branch information
vbreuss authored Apr 29, 2024
1 parent 2e1c946 commit 615a74e
Show file tree
Hide file tree
Showing 17 changed files with 139 additions and 62 deletions.
1 change: 1 addition & 0 deletions Feature.Flags.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<IS_NET8_OR_HIGHER Condition="'$(TargetFramework)' == 'net8.0'">1</IS_NET8_OR_HIGHER>

<DefineConstants Condition="'$(TargetFramework)' == 'net48' OR '$(TargetFramework)' == 'netstandard2.0'">$(DefineConstants);NETFRAMEWORK</DefineConstants>
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);CAN_SIMULATE_OTHER_OS</DefineConstants>
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_FILESYSTEM_ASYNC</DefineConstants>
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_FILESYSTEM_ENUMERATION_OPTIONS</DefineConstants>
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_PATH_JOIN</DefineConstants>
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The testing helper also supports advanced scenarios like

The companion projects [Testably.Abstractions.Compression](https://www.nuget.org/packages/Testably.Abstractions.Compression) and [Testably.Abstractions.AccessControl](https://www.nuget.org/packages/Testably.Abstractions.AccessControl) allow working with [Zip-Files](Examples/ZipFile/README.md) and [Access Control Lists](Examples/AccessControlLists/README.md) respectively.

As the test suite runs both against the mocked and the real file system, the behaviour between the two is identical.
As the test suite runs both against the mocked and the real file system, the behaviour between the two is identical and it also allows [simulating the file system on other operating systems](#simulating-other-operating-systems) (Linux, MacOS and Windows).

In addition, the following interfaces are defined:
- The `ITimeSystem` interface abstracts away time-related functionality:
Expand Down Expand Up @@ -111,6 +111,25 @@ fileSystem.Initialize()
.WithFile("foo.txt").Which(f => f.HasStringContent("some file content"));
```

### Simulating other operating systems

The `MockFileSystem` can also simulate other operating systems than the one it is currently running on. This can be achieved, by providing the corresponding `SimulationMode` in the constructor:

```csharp
var linuxFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Linux));
// The `linuxFileSystem` now behaves like a Linux file system even under Windows:
// - case-sensitive
// - slash as directory separator
var windowsFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Windows));
// The `windowsFileSystem` now behaves like a Windows file system even under Linux or MacOS:
// - multiple drives
// - case-insensitive
// - backslash as directory separator
```

By running all tests against the real file system and the simulated under Linux, MacOS and Windows, the behaviour is consistent between the native and simulated mock file systems.

### Drive management
```csharp
var fileSystem = new MockFileSystem();
Expand Down
3 changes: 3 additions & 0 deletions Source/Testably.Abstractions.Testing/Helpers/Execute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ internal partial class Execute
/// </summary>
public StringComparison StringComparisonMode { get; }

#if !CAN_SIMULATE_OTHER_OS
[Obsolete("Simulating other operating systems is not supported on .NET Framework")]
#endif
internal Execute(MockFileSystem fileSystem, SimulationMode simulationMode)
{
IsLinux = simulationMode == SimulationMode.Linux;
Expand Down
33 changes: 26 additions & 7 deletions Source/Testably.Abstractions.Testing/MockFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,29 @@ public MockFileSystem() : this(_ => { }) { }
/// <summary>
/// Initializes the <see cref="MockFileSystem" /> with the <paramref name="initializationCallback" />.
/// </summary>
internal MockFileSystem(Action<Initialization> initializationCallback)
public MockFileSystem(Action<Initialization> initializationCallback)
{
Initialization initialization = new();
initializationCallback(initialization);

SimulationMode = initialization.SimulationMode;
#if CAN_SIMULATE_OTHER_OS
Execute = SimulationMode == SimulationMode.Native
? new Execute(this)
: new Execute(this, SimulationMode);
#else
if (SimulationMode != SimulationMode.Native)
{
throw new NotSupportedException(
"Simulating other operating systems is not supported on .NET Framework");
}

Execute = new Execute(this);
#endif
StatisticsRegistration = new FileSystemStatistics(this);
using IDisposable release = StatisticsRegistration.Ignore();
RandomSystem = new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default());
RandomSystem =
new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default());
TimeSystem = new MockTimeSystem(TimeProvider.Now());
_pathMock = new PathMock(this);
_storage = new InMemoryStorage(this);
Expand Down Expand Up @@ -225,7 +236,7 @@ private void InitializeFileSystem(Initialization initialization)
/// <summary>
/// The initialization options for the <see cref="MockFileSystem" />.
/// </summary>
internal class Initialization
public class Initialization
{
/// <summary>
/// The current directory.
Expand All @@ -242,10 +253,18 @@ internal class Initialization
/// </summary>
internal SimulationMode SimulationMode { get; private set; } = SimulationMode.Native;

internal Initialization()
{
// Avoid public constructor
}

/// <summary>
/// Specify the operating system that should be simulated.
/// </summary>
internal Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
#if !CAN_SIMULATE_OTHER_OS
[Obsolete("Simulating other operating systems is not supported on .NET Framework")]
#endif
public Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
{
SimulationMode = simulationMode;
return this;
Expand All @@ -254,7 +273,7 @@ internal Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
/// <summary>
/// Use the provided <paramref name="path" /> as current directory.
/// </summary>
internal Initialization UseCurrentDirectory(string path)
public Initialization UseCurrentDirectory(string path)
{
CurrentDirectory = path;
return this;
Expand All @@ -263,7 +282,7 @@ internal Initialization UseCurrentDirectory(string path)
/// <summary>
/// Use <see cref="Directory.GetCurrentDirectory()" /> as current directory.
/// </summary>
internal Initialization UseCurrentDirectory()
public Initialization UseCurrentDirectory()
{
CurrentDirectory = System.IO.Directory.GetCurrentDirectory();
return this;
Expand All @@ -272,7 +291,7 @@ internal Initialization UseCurrentDirectory()
/// <summary>
/// Use the given <paramref name="randomProvider" /> for the <see cref="RandomSystem" />.
/// </summary>
internal Initialization UseRandomProvider(IRandomProvider randomProvider)
public Initialization UseRandomProvider(IRandomProvider randomProvider)
{
RandomProvider = randomProvider;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Testably.Abstractions.Testing.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Testably.Abstractions.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[assembly: System.CLSCompliant(true)]
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")]
namespace Testably.Abstractions.Testing.FileSystem
{
Expand Down Expand Up @@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
{
public MockFileSystem() { }
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
public System.IO.Abstractions.IDirectory Directory { get; }
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
Expand All @@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
public class Initialization
{
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
}
}
public static class MockFileSystemExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[assembly: System.CLSCompliant(true)]
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v7.0", FrameworkDisplayName=".NET 7.0")]
namespace Testably.Abstractions.Testing.FileSystem
{
Expand Down Expand Up @@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
{
public MockFileSystem() { }
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
public System.IO.Abstractions.IDirectory Directory { get; }
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
Expand All @@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
public class Initialization
{
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
}
}
public static class MockFileSystemExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[assembly: System.CLSCompliant(true)]
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
namespace Testably.Abstractions.Testing.FileSystem
{
Expand Down Expand Up @@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
{
public MockFileSystem() { }
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
public System.IO.Abstractions.IDirectory Directory { get; }
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
Expand All @@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
public class Initialization
{
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
}
}
public static class MockFileSystemExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[assembly: System.CLSCompliant(true)]
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
namespace Testably.Abstractions.Testing.FileSystem
{
Expand Down Expand Up @@ -92,6 +91,7 @@ namespace Testably.Abstractions.Testing
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
{
public MockFileSystem() { }
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
public System.IO.Abstractions.IDirectory Directory { get; }
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
Expand All @@ -110,6 +110,14 @@ namespace Testably.Abstractions.Testing
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
public class Initialization
{
[System.Obsolete("Simulating other operating systems is not supported on .NET Framework")]
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
}
}
public static class MockFileSystemExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[assembly: System.CLSCompliant(true)]
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName=".NET Standard 2.1")]
namespace Testably.Abstractions.Testing.FileSystem
{
Expand Down Expand Up @@ -92,6 +91,7 @@ namespace Testably.Abstractions.Testing
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
{
public MockFileSystem() { }
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
public System.IO.Abstractions.IDirectory Directory { get; }
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
Expand All @@ -110,6 +110,13 @@ namespace Testably.Abstractions.Testing
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
public class Initialization
{
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
}
}
public static class MockFileSystemExtensions
{
Expand Down
Loading

0 comments on commit 615a74e

Please sign in to comment.