Skip to content

Commit 6d984e2

Browse files
authored
feat: implement GetTempFileName for simulated Path (#570)
Implement the `GetTempFileName` methods for `Path` and allow initialization with `IRandomProvider` to test random edge cases.
1 parent 066fb69 commit 6d984e2

File tree

15 files changed

+134
-22
lines changed

15 files changed

+134
-22
lines changed

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ internal partial class Execute
44
{
55
private sealed class MacPath(MockFileSystem fileSystem) : LinuxPath(fileSystem)
66
{
7+
private readonly MockFileSystem _fileSystem = fileSystem;
78
private string? _tempPath;
89

910
/// <inheritdoc cref="IPath.GetTempPath()" />
1011
public override string GetTempPath()
1112
{
12-
_tempPath ??= $"/var/folders/{RandomString(2)}/{RandomString(2)}_{RandomString(27)}/T/";
13+
_tempPath ??=
14+
$"/var/folders/{RandomString(_fileSystem, 2)}/{RandomString(_fileSystem, 2)}_{RandomString(_fileSystem, 27)}/T/";
1315
return _tempPath;
1416
}
1517
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public string GetRelativePath(string relativeTo, string path)
198198
"Insecure temporary file creation methods should not be used. Use `Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())` instead.")]
199199
#endif
200200
public string GetTempFileName()
201-
=> System.IO.Path.GetTempFileName();
201+
=> CreateTempFileName(fileSystem);
202202

203203
/// <inheritdoc cref="Path.GetTempPath()" />
204204
public string GetTempPath()

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

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Diagnostics;
33
using System.Diagnostics.CodeAnalysis;
4-
using System.Linq;
54
using System.Text;
65
#if FEATURE_FILESYSTEM_NET7
76
using Testably.Abstractions.Testing.Storage;
@@ -284,7 +283,7 @@ public ReadOnlySpan<char> GetPathRoot(ReadOnlySpan<char> path)
284283

285284
/// <inheritdoc cref="IPath.GetRandomFileName()" />
286285
public string GetRandomFileName()
287-
=> $"{RandomString(8)}.{RandomString(3)}";
286+
=> $"{RandomString(fileSystem, 8)}.{RandomString(fileSystem, 3)}";
288287

289288
#if FEATURE_PATH_RELATIVE
290289
/// <inheritdoc cref="IPath.GetRelativePath(string, string)" />
@@ -306,7 +305,7 @@ public string GetRelativePath(string relativeTo, string path)
306305
"Insecure temporary file creation methods should not be used. Use `Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())` instead.")]
307306
#endif
308307
public string GetTempFileName()
309-
=> System.IO.Path.GetTempFileName();
308+
=> CreateTempFileName(fileSystem);
310309

311310
/// <inheritdoc cref="IPath.GetTempPath()" />
312311
public abstract string GetTempPath();
@@ -573,13 +572,6 @@ private string JoinInternal(string?[] paths)
573572

574573
protected abstract string NormalizeDirectorySeparators(string path);
575574

576-
protected string RandomString(int length)
577-
{
578-
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
579-
return new string(Enumerable.Repeat(chars, length)
580-
.Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray());
581-
}
582-
583575
/// <summary>
584576
/// Remove relative segments from the given path (without combining with a root).
585577
/// </summary>

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

+30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.IO;
3+
using System.Linq;
24
using System.Runtime.InteropServices;
35

46
namespace Testably.Abstractions.Testing.Helpers;
@@ -78,4 +80,32 @@ internal Execute(MockFileSystem fileSystem)
7880
: StringComparison.OrdinalIgnoreCase;
7981
Path = new NativePath(fileSystem);
8082
}
83+
84+
internal static string CreateTempFileName(MockFileSystem fileSystem)
85+
{
86+
int i = 0;
87+
string tempPath = fileSystem.Path.GetTempPath();
88+
fileSystem.Directory.CreateDirectory(tempPath);
89+
while (true)
90+
{
91+
string fileName = $"{RandomString(fileSystem, 8)}.tmp";
92+
string path = string.Concat(tempPath, fileName);
93+
try
94+
{
95+
fileSystem.File.Open(path, FileMode.CreateNew, FileAccess.Write).Dispose();
96+
return path;
97+
}
98+
catch (IOException) when (i < 100)
99+
{
100+
i++; // Don't let unforeseen circumstances cause us to loop forever
101+
}
102+
}
103+
}
104+
105+
internal static string RandomString(MockFileSystem fileSystem, int length)
106+
{
107+
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
108+
return new string(Enumerable.Repeat(chars, length)
109+
.Select(s => s[fileSystem.RandomSystem.Random.Shared.Next(s.Length)]).ToArray());
110+
}
81111
}

Source/Testably.Abstractions.Testing/MockFileSystem.cs

+16-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO;
55
using Testably.Abstractions.Testing.FileSystem;
66
using Testably.Abstractions.Testing.Helpers;
7+
using Testably.Abstractions.Testing.RandomSystem;
78
using Testably.Abstractions.Testing.Statistics;
89
using Testably.Abstractions.Testing.Storage;
910

@@ -107,7 +108,7 @@ internal MockFileSystem(Action<Initialization> initializationCallback)
107108
: new Execute(this, SimulationMode);
108109
StatisticsRegistration = new FileSystemStatistics(this);
109110
using IDisposable release = StatisticsRegistration.Ignore();
110-
RandomSystem = new MockRandomSystem();
111+
RandomSystem = new MockRandomSystem(initialization.RandomProvider ?? RandomProvider.Default());
111112
TimeSystem = new MockTimeSystem(TimeProvider.Now());
112113
_pathMock = new PathMock(this);
113114
_storage = new InMemoryStorage(this);
@@ -231,6 +232,11 @@ internal class Initialization
231232
/// </summary>
232233
internal string? CurrentDirectory { get; private set; }
233234

235+
/// <summary>
236+
/// The <see cref="IRandomProvider" /> for the <see cref="RandomSystem" />.
237+
/// </summary>
238+
internal IRandomProvider? RandomProvider { get; private set; }
239+
234240
/// <summary>
235241
/// The simulated operating system.
236242
/// </summary>
@@ -262,5 +268,14 @@ internal Initialization UseCurrentDirectory()
262268
CurrentDirectory = System.IO.Directory.GetCurrentDirectory();
263269
return this;
264270
}
271+
272+
/// <summary>
273+
/// Use the given <paramref name="randomProvider" /> for the <see cref="RandomSystem" />.
274+
/// </summary>
275+
internal Initialization UseRandomProvider(IRandomProvider randomProvider)
276+
{
277+
RandomProvider = randomProvider;
278+
return this;
279+
}
265280
}
266281
}

Source/Testably.Abstractions.Testing/MockRandomSystem.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ public MockRandomSystem() : this(Testing.RandomProvider.Default())
2626
}
2727

2828
/// <summary>
29-
/// Initializes the <see cref="MockRandomSystem" /> with the specified <paramref name="randomProviderProvider" />.
29+
/// Initializes the <see cref="MockRandomSystem" /> with the specified <paramref name="randomProvider" />.
3030
/// </summary>
31-
public MockRandomSystem(IRandomProvider randomProviderProvider)
31+
public MockRandomSystem(IRandomProvider randomProvider)
3232
{
33-
RandomProvider = randomProviderProvider;
33+
RandomProvider = randomProvider;
3434
_guidMock = new GuidMock(this);
3535
_randomFactoryMock = new RandomFactoryMock(this);
3636
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing
122122
public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem
123123
{
124124
public MockRandomSystem() { }
125-
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { }
125+
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
126126
public Testably.Abstractions.RandomSystem.IGuid Guid { get; }
127127
public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; }
128128
public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; }

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing
122122
public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem
123123
{
124124
public MockRandomSystem() { }
125-
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { }
125+
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
126126
public Testably.Abstractions.RandomSystem.IGuid Guid { get; }
127127
public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; }
128128
public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; }

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ namespace Testably.Abstractions.Testing
122122
public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem
123123
{
124124
public MockRandomSystem() { }
125-
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { }
125+
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
126126
public Testably.Abstractions.RandomSystem.IGuid Guid { get; }
127127
public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; }
128128
public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; }

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ namespace Testably.Abstractions.Testing
120120
public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem
121121
{
122122
public MockRandomSystem() { }
123-
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { }
123+
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
124124
public Testably.Abstractions.RandomSystem.IGuid Guid { get; }
125125
public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; }
126126
public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; }

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ namespace Testably.Abstractions.Testing
120120
public sealed class MockRandomSystem : Testably.Abstractions.IRandomSystem
121121
{
122122
public MockRandomSystem() { }
123-
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProviderProvider) { }
123+
public MockRandomSystem(Testably.Abstractions.Testing.RandomSystem.IRandomProvider randomProvider) { }
124124
public Testably.Abstractions.RandomSystem.IGuid Guid { get; }
125125
public Testably.Abstractions.RandomSystem.IRandomFactory Random { get; }
126126
public Testably.Abstractions.Testing.RandomSystem.IRandomProvider RandomProvider { get; }

Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/ClassGenerators/FileSystemClassGenerator.cs

+1
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ private bool IncludeSimulatedTests(ClassModel @class)
272272
"GetFullPathTests",
273273
"GetPathRootTests",
274274
"GetRandomFileNameTests",
275+
"GetTempFileNameTests",
275276
"GetTempPathTests",
276277
"HasExtensionTests",
277278
"IsPathRootedTests",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.IO;
2+
3+
namespace Testably.Abstractions.Testing.Tests.FileSystem;
4+
5+
public sealed class PathMockTests
6+
{
7+
[Theory]
8+
[InlineAutoData(SimulationMode.Native)]
9+
[InlineAutoData(SimulationMode.Linux)]
10+
[InlineAutoData(SimulationMode.MacOS)]
11+
[InlineAutoData(SimulationMode.Windows)]
12+
public void GetTempFileName_WithCollisions_ShouldThrowIOException(
13+
SimulationMode simulationMode, int fixedRandomValue)
14+
{
15+
MockFileSystem fileSystem = new(i => i
16+
.SimulatingOperatingSystem(simulationMode)
17+
.UseRandomProvider(RandomProvider.Generate(
18+
intGenerator: new RandomProvider.Generator<int>(() => fixedRandomValue))));
19+
string result = fileSystem.Path.GetTempFileName();
20+
21+
Exception? exception = Record.Exception(() =>
22+
{
23+
_ = fileSystem.Path.GetTempFileName();
24+
});
25+
26+
exception.Should().BeOfType<IOException>();
27+
fileSystem.File.Exists(result).Should().BeTrue();
28+
}
29+
}

Tests/Testably.Abstractions.Testing.Tests/MockFileSystemInitializationTests.cs

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
24
using Testably.Abstractions.Testing.Tests.TestHelpers;
35
#if NET6_0_OR_GREATER
46
#endif
@@ -124,6 +126,22 @@ public void UseCurrentDirectory_WithPath_ShouldUsePathCurrentDirectory(string pa
124126
sut.CurrentDirectory.Should().Be(path);
125127
}
126128

129+
[Theory]
130+
[AutoData]
131+
public void UseRandomProvider_ShouldUseFixedRandomValue(int fixedRandomValue)
132+
{
133+
MockFileSystem fileSystem = new(i => i
134+
.UseRandomProvider(RandomProvider.Generate(
135+
intGenerator: new RandomProvider.Generator<int>(() => fixedRandomValue))));
136+
137+
List<int> results = Enumerable.Range(1, 100)
138+
.Select(_ => fileSystem.RandomSystem.Random.New().Next())
139+
.ToList();
140+
results.Add(fileSystem.RandomSystem.Random.Shared.Next());
141+
142+
results.Should().AllBeEquivalentTo(fixedRandomValue);
143+
}
144+
127145
#region Helpers
128146

129147
public static TheoryData<SimulationMode> ValidOperatingSystems()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Testably.Abstractions.Tests.FileSystem.Path;
2+
3+
// ReSharper disable once PartialTypeWithSinglePart
4+
public abstract partial class GetTempFileNameTests<TFileSystem>
5+
: FileSystemTestBase<TFileSystem>
6+
where TFileSystem : IFileSystem
7+
{
8+
[SkippableFact]
9+
public void GetTempFileName_ShouldBeInTempPath()
10+
{
11+
string tempPath = FileSystem.Path.GetTempPath();
12+
13+
string result = FileSystem.Path.GetTempFileName();
14+
15+
result.Should().StartWith(tempPath);
16+
}
17+
18+
[SkippableFact]
19+
public void GetTempFileName_ShouldExist()
20+
{
21+
string result = FileSystem.Path.GetTempFileName();
22+
23+
FileSystem.File.Exists(result).Should().BeTrue();
24+
}
25+
}

0 commit comments

Comments
 (0)