-
Notifications
You must be signed in to change notification settings - Fork 4.7k
/
FileCleanupTestBase.cs
252 lines (212 loc) · 11.8 KB
/
FileCleanupTestBase.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
using Xunit;
namespace System.IO
{
/// <summary>Base class for test classes the use temporary files that need to be cleaned up.</summary>
public abstract partial class FileCleanupTestBase : IDisposable
{
private string fallbackGuid = Guid.NewGuid().ToString("N").Substring(0, 10);
/// <summary>Initialize the test class base. This creates the associated test directory.</summary>
protected FileCleanupTestBase(string tempDirectory = null)
{
tempDirectory ??= Path.GetTempPath();
// Use a unique test directory per test class. The test directory lives in the user's temp directory,
// and includes both the name of the test class and a random string. The test class name is included
// so that it can be easily correlated if necessary, and the random string to helps avoid conflicts if
// the same test should be run concurrently with itself (e.g. if a [Fact] method lives on a base class)
// or if some stray files were left over from a previous run.
// Make 3 attempts since we have seen this on rare occasions fail with access denied, perhaps due to machine
// configuration, and it doesn't make sense to fail arbitrary tests for this reason.
string failure = string.Empty;
for (int i = 0; i <= 2; i++)
{
// Prefix with "#" to help spot leaked files
TestDirectory = Path.Combine(tempDirectory, "#" + GetType().Name + "_" + Path.GetRandomFileName());
try
{
Directory.CreateDirectory(TestDirectory);
break;
}
catch (Exception ex)
{
failure += ex.ToString() + Environment.NewLine;
Thread.Sleep(10); // Give a transient condition like antivirus/indexing a chance to go away
}
}
Assert.True(Directory.Exists(TestDirectory), $"FileCleanupTestBase failed to create {TestDirectory}. {failure}");
}
/// <summary>Delete the associated test directory.</summary>
~FileCleanupTestBase()
{
Dispose(false);
}
/// <summary>Delete the associated test directory.</summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>Delete the associated test directory.</summary>
protected virtual void Dispose(bool disposing)
{
try
{
try
{
Directory.Delete(TestDirectory, recursive: true);
}
catch (UnauthorizedAccessException)
{
DirectoryInfo di = new DirectoryInfo(TestDirectory);
foreach (FileSystemInfo fsi in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
{
fsi.Attributes = FileAttributes.Normal;
}
Directory.Delete(TestDirectory, recursive: true);
}
}
catch { } // avoid exceptions escaping Dispose
}
/// <summary>
/// Gets the test directory into which all files and directories created by tests should be stored.
/// This directory is isolated per test class.
/// </summary>
protected string TestDirectory { get; }
protected string GetRandomFileName([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => GetTestFileName(index: null, memberName, lineNumber) + ".txt";
protected string GetRandomLinkName([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => GetTestFileName(index: null, memberName, lineNumber) + ".link";
protected string GetRandomDirName([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => GetTestFileName(index: null, memberName, lineNumber) + "_dir";
protected string GetRandomFilePath([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => Path.Combine(TestDirectoryActualCasing, GetRandomFileName(memberName, lineNumber));
protected string GetRandomLinkPath([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => Path.Combine(TestDirectoryActualCasing, GetRandomLinkName(memberName, lineNumber));
protected string GetRandomDirPath([CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) => Path.Combine(TestDirectoryActualCasing, GetRandomDirName(memberName, lineNumber));
private string _testDirectoryActualCasing;
private string TestDirectoryActualCasing => _testDirectoryActualCasing ??= GetTestDirectoryActualCasing();
/// <summary>Gets a test file full path that is associated with the call site.</summary>
/// <param name="index">An optional index value to use as a suffix on the file name. Typically a loop index.</param>
/// <param name="memberName">The member name of the function calling this method.</param>
/// <param name="lineNumber">The line number of the function calling this method.</param>
protected virtual string GetTestFilePath(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0) =>
Path.Combine(TestDirectory, GetTestFileName(index, memberName, lineNumber));
/// <summary>Gets a test file name that is associated with the call site.</summary>
/// <param name="index">An optional index value to use as a suffix on the file name. Typically a loop index.</param>
/// <param name="memberName">The member name of the function calling this method.</param>
/// <param name="lineNumber">The line number of the function calling this method.</param>
protected string GetTestFileName(int? index = null, [CallerMemberName] string memberName = null, [CallerLineNumber] int lineNumber = 0)
{
string testFileName = PathGenerator.GenerateTestFileName(index, memberName, lineNumber);
string testFilePath = Path.Combine(TestDirectory, testFileName);
const int maxLength = 260 - 5; // Windows MAX_PATH minus a bit
int excessLength = testFilePath.Length - maxLength;
if (excessLength > 0)
{
// The path will be too long for Windows -- can we
// trim memberName to fix it?
if (excessLength < memberName.Length + "...".Length)
{
// Take a chunk out of the middle as perhaps it's the least interesting part of the name
int halfMemberNameLength = (int)Math.Floor((double)memberName.Length / 2);
int halfExcessLength = (int)Math.Ceiling((double)excessLength / 2);
memberName = memberName.Substring(0, halfMemberNameLength - halfExcessLength) + "..." + memberName.Substring(halfMemberNameLength + halfExcessLength);
testFileName = PathGenerator.GenerateTestFileName(index, memberName, lineNumber);
testFilePath = Path.Combine(TestDirectory, testFileName);
}
else
{
return fallbackGuid;
}
}
Debug.Assert(testFilePath.Length <= maxLength + "...".Length);
return testFileName;
}
protected static string GetNamedPipeServerStreamName()
{
if (PlatformDetection.IsInAppContainer)
{
return @"LOCAL\" + Guid.NewGuid().ToString("N");
}
if (PlatformDetection.IsWindows)
{
return Guid.NewGuid().ToString("N");
}
if (!PlatformDetection.FileCreateCaseSensitive)
{
return $"/tmp/{Guid.NewGuid().ToString("N")}";
}
const int MinUdsPathLength = 104; // required min is 92, but every platform we currently target is at least 104
const int MinAvailableForSufficientRandomness = 5; // we want enough randomness in the name to avoid conflicts between concurrent tests
string prefix = Path.Combine(Path.GetTempPath(), "CoreFxPipe_");
int availableLength = MinUdsPathLength - prefix.Length - 1; // 1 - for possible null terminator
Assert.True(availableLength >= MinAvailableForSufficientRandomness, $"UDS prefix {prefix} length {prefix.Length} is too long");
StringBuilder sb = new(availableLength);
Random random = new Random();
for (int i = 0; i < availableLength; i++)
{
sb.Append((char)('a' + random.Next(0, 26)));
}
return sb.ToString();
}
// Some Windows versions like Windows Nano Server have the %TEMP% environment variable set to "C:\TEMP" but the
// actual folder name is "C:\Temp", which prevents asserting path values using Assert.Equal due to case sensitiveness.
// So instead of using TestDirectory directly, we retrieve the real path with proper casing of the initial folder path.
private unsafe string GetTestDirectoryActualCasing()
{
if (!PlatformDetection.IsWindows)
return TestDirectory;
try
{
using SafeFileHandle handle = Interop.Kernel32.CreateFile(
TestDirectory,
dwDesiredAccess: 0,
dwShareMode: FileShare.ReadWrite | FileShare.Delete,
dwCreationDisposition: FileMode.Open,
dwFlagsAndAttributes:
Interop.Kernel32.FileOperations.OPEN_EXISTING |
Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS // Necessary to obtain a handle to a directory
);
if (!handle.IsInvalid)
{
char[] buffer = new char[4096];
uint result;
fixed (char* bufPtr = buffer)
{
result = Interop.Kernel32.GetFinalPathNameByHandle(handle, bufPtr, (uint)buffer.Length, Interop.Kernel32.FILE_NAME_NORMALIZED);
}
if (result == 0)
{
throw new Win32Exception();
}
Debug.Assert(result <= buffer.Length);
// Remove extended prefix
int skip = PathInternal.IsExtended(buffer) ? 4 : 0;
return new string(buffer, skip, (int)result - skip);
}
}
catch { }
return TestDirectory;
}
protected string CreateTestDirectory(params string[] paths)
{
string dir = Path.Combine(paths);
Assert.True(Path.IsPathRooted(dir));
Directory.CreateDirectory(dir);
return dir;
}
protected string CreateTestDirectory() => CreateTestDirectory(GetTestFilePath());
protected string CreateTestFile(params string[] paths)
{
string file = Path.Combine(paths);
Assert.True(Path.IsPathRooted(file));
Directory.CreateDirectory(Path.GetDirectoryName(file));
File.Create(file).Dispose();
return file;
}
protected string CreateTestFile() => CreateTestFile(GetTestFilePath());
}
}