diff options
Diffstat (limited to 'src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs')
-rw-r--r-- | src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs new file mode 100644 index 00000000000..8aa81f08492 --- /dev/null +++ b/src/libraries/Common/tests/TestUtilities/System/IO/FileCleanupTestBase.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace System.IO +{ + /// <summary>Base class for test classes the use temporary files that need to be cleaned up.</summary> + public abstract class FileCleanupTestBase : IDisposable + { + private static readonly Lazy<bool> s_isElevated = new Lazy<bool>(() => AdminHelpers.IsProcessElevated()); + + private string fallbackGuid = Guid.NewGuid().ToString("N").Substring(0, 10); + + protected static bool IsProcessElevated => s_isElevated.Value; + + /// <summary>Initialize the test class base. This creates the associated test directory.</summary> + protected FileCleanupTestBase() + { + // 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++) + { + TestDirectory = Path.Combine(Path.GetTempPath(), 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) + { + // No managed resources to clean up, so disposing is ignored. + + try { 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; } + + /// <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 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 = 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 + memberName = memberName.Substring(0, memberName.Length / 2 - excessLength / 2) + "..." + memberName.Substring(memberName.Length / 2 + excessLength / 2); + + testFileName = GenerateTestFileName(index, memberName, lineNumber); + testFilePath = Path.Combine(TestDirectory, testFileName); + } + else + { + return fallbackGuid; + } + } + + Debug.Assert(testFilePath.Length <= maxLength + "...".Length); + + return testFileName; + } + + private string GenerateTestFileName(int? index, string memberName, int lineNumber) => + string.Format( + index.HasValue ? "{0}_{1}_{2}_{3}" : "{0}_{1}_{3}", + memberName ?? "TestBase", + lineNumber, + index.GetValueOrDefault(), + Guid.NewGuid().ToString("N").Substring(0, 8)); // randomness to avoid collisions between derived test classes using same base method concurrently + } +} |