diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.MacPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.MacPath.cs index d8dca45df..c0494badd 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.MacPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.MacPath.cs @@ -4,12 +4,14 @@ internal partial class Execute { private sealed class MacPath(MockFileSystem fileSystem) : LinuxPath(fileSystem) { + private readonly MockFileSystem _fileSystem = fileSystem; private string? _tempPath; /// public override string GetTempPath() { - _tempPath ??= $"/var/folders/{RandomString(2)}/{RandomString(2)}_{RandomString(27)}/T/"; + _tempPath ??= + $"/var/folders/{RandomString(_fileSystem, 2)}/{RandomString(_fileSystem, 2)}_{RandomString(_fileSystem, 27)}/T/"; return _tempPath; } } diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs index 8f49b0519..f5463a80f 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.NativePath.cs @@ -198,7 +198,7 @@ public string GetRelativePath(string relativeTo, string path) "Insecure temporary file creation methods should not be used. Use `Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())` instead.")] #endif public string GetTempFileName() - => System.IO.Path.GetTempFileName(); + => CreateTempFileName(fileSystem); /// public string GetTempPath() diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs index 3d2683235..9eba6c1d3 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.SimulatedPath.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; #if FEATURE_FILESYSTEM_NET7 using Testably.Abstractions.Testing.Storage; @@ -284,7 +283,7 @@ public ReadOnlySpan GetPathRoot(ReadOnlySpan path) /// public string GetRandomFileName() - => $"{RandomString(8)}.{RandomString(3)}"; + => $"{RandomString(fileSystem, 8)}.{RandomString(fileSystem, 3)}"; #if FEATURE_PATH_RELATIVE /// @@ -306,7 +305,7 @@ public string GetRelativePath(string relativeTo, string path) "Insecure temporary file creation methods should not be used. Use `Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())` instead.")] #endif public string GetTempFileName() - => System.IO.Path.GetTempFileName(); + => CreateTempFileName(fileSystem); /// public abstract string GetTempPath(); @@ -573,13 +572,6 @@ private string JoinInternal(string?[] paths) protected abstract string NormalizeDirectorySeparators(string path); - protected string RandomString(int length) - { - const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray()); - } - /// /// Remove relative segments from the given path (without combining with a root). /// diff --git a/Source/Testably.Abstractions.Testing/Helpers/Execute.cs b/Source/Testably.Abstractions.Testing/Helpers/Execute.cs index 0ed8ec070..3bedf516b 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/Execute.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/Execute.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; namespace Testably.Abstractions.Testing.Helpers; @@ -78,4 +80,32 @@ internal Execute(MockFileSystem fileSystem) : StringComparison.OrdinalIgnoreCase; Path = new NativePath(fileSystem); } + + internal static string CreateTempFileName(MockFileSystem fileSystem) + { + int i = 0; + string tempPath = fileSystem.Path.GetTempPath(); + fileSystem.Directory.CreateDirectory(tempPath); + while (true) + { + string fileName = $"{RandomString(fileSystem, 8)}.tmp"; + string path = string.Concat(tempPath, fileName); + try + { + fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write).Dispose(); + return path; + } + catch (IOException) when (i < 100) + { + i++; // Don't let unforeseen circumstances cause us to loop forever + } + } + } + + internal static string RandomString(MockFileSystem fileSystem, int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray()); + } } diff --git a/Source/Testably.Abstractions.Testing/MockFileSystem.cs b/Source/Testably.Abstractions.Testing/MockFileSystem.cs index c02ab4dba..a33520a48 100644 --- a/Source/Testably.Abstractions.Testing/MockFileSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockFileSystem.cs @@ -4,6 +4,7 @@ using System.IO; using Testably.Abstractions.Testing.FileSystem; using Testably.Abstractions.Testing.Helpers; +using Testably.Abstractions.Testing.RandomSystem; using Testably.Abstractions.Testing.Statistics; using Testably.Abstractions.Testing.Storage; @@ -107,7 +108,7 @@ internal MockFileSystem(Action initializationCallback) : new Execute(this, SimulationMode); StatisticsRegistration = new FileSystemStatistics(this); using IDisposable release = StatisticsRegistration.Ignore(); - RandomSystem = new MockRandomSystem(); + RandomSystem = new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default()); TimeSystem = new MockTimeSystem(TimeProvider.Now()); _pathMock = new PathMock(this); _storage = new InMemoryStorage(this); @@ -231,6 +232,11 @@ internal class Initialization /// internal string? CurrentDirectory { get; private set; } + /// + /// The for the . + /// + internal IRandomProvider? RandomProvider { get; private set; } + /// /// The simulated operating system. /// @@ -262,5 +268,14 @@ internal Initialization UseCurrentDirectory() CurrentDirectory = System.IO.Directory.GetCurrentDirectory(); return this; } + + /// + /// Use the given for the . + /// + internal Initialization UseRandomProvider(IRandomProvider randomProvider) + { + RandomProvider = randomProvider; + return this; + } } } diff --git a/Source/Testably.Abstractions.Testing/MockRandomSystem.cs b/Source/Testably.Abstractions.Testing/MockRandomSystem.cs index 3e200504b..3cb5c07e8 100644 --- a/Source/Testably.Abstractions.Testing/MockRandomSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockRandomSystem.cs @@ -26,11 +26,11 @@ public MockRandomSystem() : this(Testing.RandomProvider.Default()) } /// - /// Initializes the with the specified . + /// Initializes the with the specified . /// - public MockRandomSystem(IRandomProvider randomProviderProvider) + public MockRandomSystem(IRandomProvider randomProvider) { - RandomProvider = randomProviderProvider; + RandomProvider = randomProvider; _guidMock = new GuidMock(this); _randomFactoryMock = new RandomFactoryMock(this); } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt index d40ddc537..2c6aaeeca 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net6.0.txt @@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem { public MockRandomSystem() { } - public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { } + public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { } public Testably.Abstractions.RandomSystem.IGuid Guid { get; } public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; } public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net7.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net7.0.txt index bf1e5c5f3..cf20f78ea 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net7.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net7.0.txt @@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem { public MockRandomSystem() { } - public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { } + public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { } public Testably.Abstractions.RandomSystem.IGuid Guid { get; } public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; } public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt index 0ca789e67..d7c837710 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_net8.0.txt @@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem { public MockRandomSystem() { } - public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { } + public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { } public Testably.Abstractions.RandomSystem.IGuid Guid { get; } public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; } public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt index 7c3ac4cb6..48435f5b5 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.0.txt @@ -120,7 +120,7 @@ namespace Testably.Abstractions.Testing public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem { public MockRandomSystem() { } - public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { } + public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { } public Testably.Abstractions.RandomSystem.IGuid Guid { get; } public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; } public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt index 06a0a60e9..c3e4957dd 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions.Testing_netstandard2.1.txt @@ -120,7 +120,7 @@ namespace Testably.Abstractions.Testing public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem { public MockRandomSystem() { } - public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { } + public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { } public Testably.Abstractions.RandomSystem.IGuid Guid { get; } public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; } public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; } diff --git a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs index a514bce74..fea21b4e1 100644 --- a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs +++ b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs @@ -272,6 +272,7 @@ private bool IncludeSimulatedTests(ClassModel @class) "GetFullPathTests", "GetPathRootTests", "GetRandomFileNameTests", + "GetTempFileNameTests", "GetTempPathTests", "HasExtensionTests", "IsPathRootedTests", diff --git a/Tests/Testably.Abstractions.Testing.Tests/FileSystem/PathMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/PathMockTests.cs new file mode 100644 index 000000000..0b261da4f --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/FileSystem/PathMockTests.cs @@ -0,0 +1,29 @@ +using System.IO; + +namespace Testably.Abstractions.Testing.Tests.FileSystem; + +public sealed class PathMockTests +{ + [Theory] + [InlineAutoData(SimulationMode.Native)] + [InlineAutoData(SimulationMode.Linux)] + [InlineAutoData(SimulationMode.MacOS)] + [InlineAutoData(SimulationMode.Windows)] + public void GetTempFileName_WithCollisions_ShouldThrowIOException( + SimulationMode simulationMode, int fixedRandomValue) + { + MockFileSystem fileSystem = new(i => i + .SimulatingOperatingSystem(simulationMode) + .UseRandomProvider(RandomProvider.Generate( + intGenerator: new RandomProvider.Generator(() => fixedRandomValue)))); + string result = fileSystem.Path.GetTempFileName(); + + Exception? exception = Record.Exception(() => + { + _ = fileSystem.Path.GetTempFileName(); + }); + + exception.Should().BeOfType(); + fileSystem.File.Exists(result).Should().BeTrue(); + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/MockFileSystemInitializationTests.cs b/Tests/Testably.Abstractions.Testing.Tests/MockFileSystemInitializationTests.cs index 8ca2b3a29..9de14321c 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/MockFileSystemInitializationTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/MockFileSystemInitializationTests.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using Testably.Abstractions.Testing.Tests.TestHelpers; #if NET6_0_OR_GREATER #endif @@ -124,6 +126,22 @@ public void UseCurrentDirectory_WithPath_ShouldUsePathCurrentDirectory(string pa sut.CurrentDirectory.Should().Be(path); } + [Theory] + [AutoData] + public void UseRandomProvider_ShouldUseFixedRandomValue(int fixedRandomValue) + { + MockFileSystem fileSystem = new(i => i + .UseRandomProvider(RandomProvider.Generate( + intGenerator: new RandomProvider.Generator(() => fixedRandomValue)))); + + List results = Enumerable.Range(1, 100) + .Select(_ => fileSystem.RandomSystem.Random.New().Next()) + .ToList(); + results.Add(fileSystem.RandomSystem.Random.Shared.Next()); + + results.Should().AllBeEquivalentTo(fixedRandomValue); + } + #region Helpers public static TheoryData ValidOperatingSystems() diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/Path/GetTempFileNameTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/Path/GetTempFileNameTests.cs new file mode 100644 index 000000000..c49cd568e --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/FileSystem/Path/GetTempFileNameTests.cs @@ -0,0 +1,25 @@ +namespace Testably.Abstractions.Tests.FileSystem.Path; + +// ReSharper disable once PartialTypeWithSinglePart +public abstract partial class GetTempFileNameTests + : FileSystemTestBase + where TFileSystem : IFileSystem +{ + [SkippableFact] + public void GetTempFileName_ShouldBeInTempPath() + { + string tempPath = FileSystem.Path.GetTempPath(); + + string result = FileSystem.Path.GetTempFileName(); + + result.Should().StartWith(tempPath); + } + + [SkippableFact] + public void GetTempFileName_ShouldExist() + { + string result = FileSystem.Path.GetTempFileName(); + + FileSystem.File.Exists(result).Should().BeTrue(); + } +}