Skip to content

Commit 615a74e

Browse files
authored
feat: allow simulating other operating systems (#576)
* 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
1 parent 2e1c946 commit 615a74e

File tree

17 files changed

+139
-62
lines changed

17 files changed

+139
-62
lines changed

Feature.Flags.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<IS_NET8_OR_HIGHER Condition="'$(TargetFramework)' == 'net8.0'">1</IS_NET8_OR_HIGHER>
88

99
<DefineConstants Condition="'$(TargetFramework)' == 'net48' OR '$(TargetFramework)' == 'netstandard2.0'">$(DefineConstants);NETFRAMEWORK</DefineConstants>
10+
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);CAN_SIMULATE_OTHER_OS</DefineConstants>
1011
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_FILESYSTEM_ASYNC</DefineConstants>
1112
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_FILESYSTEM_ENUMERATION_OPTIONS</DefineConstants>
1213
<DefineConstants Condition="'$(IS_NET21_OR_HIGHER)' == '1'">$(DefineConstants);FEATURE_PATH_JOIN</DefineConstants>

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The testing helper also supports advanced scenarios like
1414

1515
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.
1616

17-
As the test suite runs both against the mocked and the real file system, the behaviour between the two is identical.
17+
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).
1818

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

114+
### Simulating other operating systems
115+
116+
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:
117+
118+
```csharp
119+
var linuxFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Linux));
120+
// The `linuxFileSystem` now behaves like a Linux file system even under Windows:
121+
// - case-sensitive
122+
// - slash as directory separator
123+
124+
var windowsFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Windows));
125+
// The `windowsFileSystem` now behaves like a Windows file system even under Linux or MacOS:
126+
// - multiple drives
127+
// - case-insensitive
128+
// - backslash as directory separator
129+
```
130+
131+
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.
132+
114133
### Drive management
115134
```csharp
116135
var fileSystem = new MockFileSystem();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ internal partial class Execute
4040
/// </summary>
4141
public StringComparison StringComparisonMode { get; }
4242

43+
#if !CAN_SIMULATE_OTHER_OS
44+
[Obsolete("Simulating other operating systems is not supported on .NET Framework")]
45+
#endif
4346
internal Execute(MockFileSystem fileSystem, SimulationMode simulationMode)
4447
{
4548
IsLinux = simulationMode == SimulationMode.Linux;

Source/Testably.Abstractions.Testing/MockFileSystem.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,29 @@ public MockFileSystem() : this(_ => { }) { }
9797
/// <summary>
9898
/// Initializes the <see cref="MockFileSystem" /> with the <paramref name="initializationCallback" />.
9999
/// </summary>
100-
internal MockFileSystem(Action<Initialization> initializationCallback)
100+
public MockFileSystem(Action<Initialization> initializationCallback)
101101
{
102102
Initialization initialization = new();
103103
initializationCallback(initialization);
104104

105105
SimulationMode = initialization.SimulationMode;
106+
#if CAN_SIMULATE_OTHER_OS
106107
Execute = SimulationMode == SimulationMode.Native
107108
? new Execute(this)
108109
: new Execute(this, SimulationMode);
110+
#else
111+
if (SimulationMode != SimulationMode.Native)
112+
{
113+
throw new NotSupportedException(
114+
"Simulating other operating systems is not supported on .NET Framework");
115+
}
116+
117+
Execute = new Execute(this);
118+
#endif
109119
StatisticsRegistration = new FileSystemStatistics(this);
110120
using IDisposable release = StatisticsRegistration.Ignore();
111-
RandomSystem = new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default());
121+
RandomSystem =
122+
new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default());
112123
TimeSystem = new MockTimeSystem(TimeProvider.Now());
113124
_pathMock = new PathMock(this);
114125
_storage = new InMemoryStorage(this);
@@ -225,7 +236,7 @@ private void InitializeFileSystem(Initialization initialization)
225236
/// <summary>
226237
/// The initialization options for the <see cref="MockFileSystem" />.
227238
/// </summary>
228-
internal class Initialization
239+
public class Initialization
229240
{
230241
/// <summary>
231242
/// The current directory.
@@ -242,10 +253,18 @@ internal class Initialization
242253
/// </summary>
243254
internal SimulationMode SimulationMode { get; private set; } = SimulationMode.Native;
244255

256+
internal Initialization()
257+
{
258+
// Avoid public constructor
259+
}
260+
245261
/// <summary>
246262
/// Specify the operating system that should be simulated.
247263
/// </summary>
248-
internal Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
264+
#if !CAN_SIMULATE_OTHER_OS
265+
[Obsolete("Simulating other operating systems is not supported on .NET Framework")]
266+
#endif
267+
public Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
249268
{
250269
SimulationMode = simulationMode;
251270
return this;
@@ -254,7 +273,7 @@ internal Initialization SimulatingOperatingSystem(SimulationMode simulationMode)
254273
/// <summary>
255274
/// Use the provided <paramref name="path" /> as current directory.
256275
/// </summary>
257-
internal Initialization UseCurrentDirectory(string path)
276+
public Initialization UseCurrentDirectory(string path)
258277
{
259278
CurrentDirectory = path;
260279
return this;
@@ -263,7 +282,7 @@ internal Initialization UseCurrentDirectory(string path)
263282
/// <summary>
264283
/// Use <see cref="Directory.GetCurrentDirectory()" /> as current directory.
265284
/// </summary>
266-
internal Initialization UseCurrentDirectory()
285+
public Initialization UseCurrentDirectory()
267286
{
268287
CurrentDirectory = System.IO.Directory.GetCurrentDirectory();
269288
return this;
@@ -272,7 +291,7 @@ internal Initialization UseCurrentDirectory()
272291
/// <summary>
273292
/// Use the given <paramref name="randomProvider" /> for the <see cref="RandomSystem" />.
274293
/// </summary>
275-
internal Initialization UseRandomProvider(IRandomProvider randomProvider)
294+
public Initialization UseRandomProvider(IRandomProvider randomProvider)
276295
{
277296
RandomProvider = randomProvider;
278297
return this;

Source/Testably.Abstractions.Testing/Testably.Abstractions.Testing.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
2222
<_Parameter1>Testably.Abstractions.Testing.Tests</_Parameter1>
2323
</AssemblyAttribute>
24-
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
25-
<_Parameter1>Testably.Abstractions.Tests</_Parameter1>
26-
</AssemblyAttribute>
2724
</ItemGroup>
2825

2926
<ItemGroup>

Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[assembly: System.CLSCompliant(true)]
22
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
33
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
4-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
54
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")]
65
namespace Testably.Abstractions.Testing.FileSystem
76
{
@@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
9493
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
9594
{
9695
public MockFileSystem() { }
96+
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
9797
public System.IO.Abstractions.IDirectory Directory { get; }
9898
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
9999
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
@@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
112112
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
113113
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
114114
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
115+
public class Initialization
116+
{
117+
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
118+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
119+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
120+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
121+
}
115122
}
116123
public static class MockFileSystemExtensions
117124
{

Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net7.0.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[assembly: System.CLSCompliant(true)]
22
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
33
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
4-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
54
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v7.0", FrameworkDisplayName=".NET 7.0")]
65
namespace Testably.Abstractions.Testing.FileSystem
76
{
@@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
9493
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
9594
{
9695
public MockFileSystem() { }
96+
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
9797
public System.IO.Abstractions.IDirectory Directory { get; }
9898
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
9999
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
@@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
112112
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
113113
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
114114
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
115+
public class Initialization
116+
{
117+
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
118+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
119+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
120+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
121+
}
115122
}
116123
public static class MockFileSystemExtensions
117124
{

Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[assembly: System.CLSCompliant(true)]
22
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
33
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
4-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
54
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
65
namespace Testably.Abstractions.Testing.FileSystem
76
{
@@ -94,6 +93,7 @@ namespace Testably.Abstractions.Testing
9493
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
9594
{
9695
public MockFileSystem() { }
96+
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
9797
public System.IO.Abstractions.IDirectory Directory { get; }
9898
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
9999
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
@@ -112,6 +112,13 @@ namespace Testably.Abstractions.Testing
112112
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
113113
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
114114
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
115+
public class Initialization
116+
{
117+
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
118+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
119+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
120+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
121+
}
115122
}
116123
public static class MockFileSystemExtensions
117124
{

Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[assembly: System.CLSCompliant(true)]
22
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
33
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
4-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
54
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
65
namespace Testably.Abstractions.Testing.FileSystem
76
{
@@ -92,6 +91,7 @@ namespace Testably.Abstractions.Testing
9291
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
9392
{
9493
public MockFileSystem() { }
94+
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
9595
public System.IO.Abstractions.IDirectory Directory { get; }
9696
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
9797
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
@@ -110,6 +110,14 @@ namespace Testably.Abstractions.Testing
110110
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
111111
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
112112
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
113+
public class Initialization
114+
{
115+
[System.Obsolete("Simulating other operating systems is not supported on .NET Framework")]
116+
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
117+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
118+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
119+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
120+
}
113121
}
114122
public static class MockFileSystemExtensions
115123
{

Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[assembly: System.CLSCompliant(true)]
22
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/Testably/Testably.Abstractions.git")]
33
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Testing.Tests")]
4-
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Testably.Abstractions.Tests")]
54
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName=".NET Standard 2.1")]
65
namespace Testably.Abstractions.Testing.FileSystem
76
{
@@ -92,6 +91,7 @@ namespace Testably.Abstractions.Testing
9291
public sealed class MockFileSystem : System.IO.Abstractions.IFileSystem
9392
{
9493
public MockFileSystem() { }
94+
public MockFileSystem(System.Action<Testably.Abstractions.Testing.MockFileSystem.Initialization> initializationCallback) { }
9595
public System.IO.Abstractions.IDirectory Directory { get; }
9696
public System.IO.Abstractions.IDirectoryInfoFactory DirectoryInfo { get; }
9797
public System.IO.Abstractions.IDriveInfoFactory DriveInfo { get; }
@@ -110,6 +110,13 @@ namespace Testably.Abstractions.Testing
110110
public Testably.Abstractions.Testing.MockFileSystem WithAccessControlStrategy(Testably.Abstractions.Testing.FileSystem.IAccessControlStrategy accessControlStrategy) { }
111111
public Testably.Abstractions.Testing.MockFileSystem WithDrive(string? drive, System.Action<Testably.Abstractions.Testing.Storage.IStorageDrive>? driveCallback = null) { }
112112
public Testably.Abstractions.Testing.MockFileSystem WithSafeFileHandleStrategy(Testably.Abstractions.Testing.FileSystem.ISafeFileHandleStrategy safeFileHandleStrategy) { }
113+
public class Initialization
114+
{
115+
public Testably.Abstractions.Testing.MockFileSystem.Initialization SimulatingOperatingSystem(Testably.Abstractions.Testing.SimulationMode simulationMode) { }
116+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory() { }
117+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseCurrentDirectory(string path) { }
118+
public Testably.Abstractions.Testing.MockFileSystem.Initialization UseRandomProvider(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
119+
}
113120
}
114121
public static class MockFileSystemExtensions
115122
{

0 commit comments

Comments
 (0)