File: TestAssetsManager.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.TestFramework\Microsoft.NET.TestFramework.csproj (Microsoft.NET.TestFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
 
namespace Microsoft.NET.TestFramework
{
    public class TestAssetsManager
    {
        public string TestAssetsRoot { get; private set; }
 
        private List<string> TestDestinationDirectories { get; } = new List<string>();
 
        protected ITestOutputHelper Log { get; }
 
        public TestAssetsManager(ITestOutputHelper log)
        {
            var testAssetsDirectory = TestContext.Current.TestAssetsDirectory;
            Log = log;
 
            if (!Directory.Exists(testAssetsDirectory))
            {
                throw new DirectoryNotFoundException($"Directory not found: '{testAssetsDirectory}'");
            }
 
            TestAssetsRoot = testAssetsDirectory;
        }
 
        /// <summary>
        /// Creates a new 'bubble' for the given test asset project in a subdirectory
        /// of the current test execution context, scoped by the calling method and optional unique identifier.
        /// </summary>
        public TestAsset CopyTestAsset(
            string testProjectName,
            [CallerMemberName] string callingMethod = "",
            [CallerFilePath] string? callerFilePath = null,
            string? identifier = "",
            string testAssetSubdirectory = "",
            string? testDestinationDirectory = null,
            bool allowCopyIfPresent = false)
        {
            var testProjectDirectory = GetAndValidateTestProjectDirectory(testProjectName, testAssetSubdirectory);
 
            var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
            testDestinationDirectory ??= GetTestDestinationDirectoryPath(testProjectName, callingMethod + "_" + fileName, identifier, allowCopyIfPresent);
            TestDestinationDirectories.Add(testDestinationDirectory);
 
            var testAsset = new TestAsset(testProjectDirectory, testDestinationDirectory, TestContext.Current.SdkVersion, Log);
            return testAsset;
        }
 
        /// <summary>
        /// Writes an in-memory test project onto the disk.
        /// </summary>
        /// <param name="testProject">The testProject used to create a testAsset with.</param>
        /// <param name="callingMethod">Defaults to the name of the caller function (presumably the test).
        /// Used to prevent file collisions on tests which share the same test project.</param>
        /// <param name="identifier">Use this for theories.
        /// Pass in the unique theory parameters that can indentify that theory from others.
        /// The Identifier is used to distinguish between theory child tests.  Generally it should be created using a combination of all of the theory parameter values.
        /// This is distinct from the test project name and is used to prevent file collisions between theory tests that use the same test project.</param>
        /// <returns>A new TestAsset directory for the TestProject.</returns>
        public TestAsset CreateTestProject(
            TestProject testProject,
            [CallerMemberName] string callingMethod = "",
            string? identifier = "")
        {
            var testDestinationDirectory =
                GetTestDestinationDirectoryPath(testProject.Name, callingMethod, identifier);
            TestDestinationDirectories.Add(testDestinationDirectory);
 
            var testAsset = CreateTestProjectsInDirectory(new List<TestProject>() { testProject }, testDestinationDirectory);
            testAsset.TestProject = testProject;
 
            return testAsset;
        }
 
        /// <summary>
        /// Creates a list of test projects and adds them to a solution
        /// </summary>
        /// <param name="testProjects">The in-memory test projects to write to disk</param>
        /// <param name="callingMethod">Defaults to the name of the caller function (presumably the test).
        /// Used to prevent file collisions on tests which share the same test project.</param>
        /// <param name="identifier">Use this for theories.
        /// Pass in the unique theory parameters that can indentify that theory from others.
        /// The Identifier is used to distinguish between theory child tests.  Generally it should be created using a combination of all of the theory parameter values.
        /// This is distinct from the test project name and is used to prevent file collisions between theory tests that use the same test project.</param>
        /// <param name="targetExtension">The extension type of the desired test project, e.g. .csproj, or .fsproj.</param>
        /// <returns>A new TestAsset directory with the solution and test projects in it.</returns>
        public TestAsset CreateTestProjects(
            IEnumerable<TestProject> testProjects,
            [CallerMemberName] string callingMethod = "",
            string identifier = "")
        {
            var testDestinationDirectory =
                GetTestDestinationDirectoryPath(callingMethod, callingMethod, identifier);
            TestDestinationDirectories.Add(testDestinationDirectory);
 
            var testAsset = CreateTestProjectsInDirectory(testProjects, testDestinationDirectory);
 
            var slnCreationResult = new DotnetNewCommand(Log, "sln", "--format", "sln")
                .WithVirtualHive()
                .WithWorkingDirectory(testDestinationDirectory)
                .Execute();
 
            if (slnCreationResult.ExitCode != 0)
            {
                throw new Exception($"This test failed during a call to dotnet new. If {testDestinationDirectory} is valid, it's likely this test is failing because of dotnet new. If there are failing .NET new tests, please fix those and then see if this test still fails.");
            }
 
            foreach (var testProject in testProjects)
            {
                new DotnetCommand(Log, "sln", "add", testProject.Name ?? string.Empty)
                    .WithWorkingDirectory(testDestinationDirectory)
                    .Execute()
                    .Should()
                    .Pass();
            }
 
            return testAsset;
        }
 
        private TestAsset CreateTestProjectsInDirectory(
            IEnumerable<TestProject> testProjects,
            string testDestinationDirectory)
        {
            var testAsset = new TestAsset(testDestinationDirectory, TestContext.Current.SdkVersion, Log);
 
            Stack<TestProject> projectStack = new(testProjects);
            HashSet<TestProject> createdProjects = new();
 
            while (projectStack.Count > 0)
            {
                var project = projectStack.Pop();
                if (!createdProjects.Contains(project))
                {
                    project.Create(testAsset, TestAssetsRoot);
                    createdProjects.Add(project);
 
                    foreach (var referencedProject in project.ReferencedProjects)
                    {
                        projectStack.Push(referencedProject);
                    }
                }
            }
 
            return testAsset;
        }
 
        public TestDirectory CreateTestDirectory([CallerMemberName] string? testName = null, string? identifier = null, string? baseDirectory = null)
        {
            string dir = GetTestDestinationDirectoryPath(testName, testName, identifier ?? string.Empty, baseDirectory: baseDirectory);
            return new TestDirectory(dir, TestContext.Current.SdkVersion);
        }
 
        public string GetAndValidateTestProjectDirectory(string testProjectName, string testAssetSubdirectory = "")
        {
            if (string.IsNullOrEmpty(testAssetSubdirectory))
            {
                testAssetSubdirectory = "TestProjects";
            }
            string testProjectDirectory = Path.Combine(TestAssetsRoot, testAssetSubdirectory, testProjectName);
 
            if (!Directory.Exists(testProjectDirectory))
            {
                throw new DirectoryNotFoundException($"Cannot find test project directory '{testProjectDirectory}'");
            }
 
            return testProjectDirectory;
        }
 
        public static string GetTestDestinationDirectoryPath(
            string? testProjectName,
            string? callingMethodAndFileName,
            string? identifier,
            bool allowCopyIfPresent = false,
            string? baseDirectory = null)
        {
            baseDirectory ??= TestContext.Current.TestExecutionDirectory;
            var directoryName = new StringBuilder(callingMethodAndFileName).Append(identifier);
 
            if (testProjectName != callingMethodAndFileName)
            {
                directoryName = directoryName.Append(testProjectName);
            }
 
            // We need to ensure the directory name isn't over 24 characters in length
            if (directoryName.Length > 24)
            {
                using var sha256 = SHA256.Create();
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(directoryName.ToString()));
 
                directoryName = directoryName.Remove(13, directoryName.Length - 13)
                                             .Append("---");
 
                directoryName = directoryName.AppendFormat("{0:X2}", hash[0])
                                             .AppendFormat("{0:X2}", hash[1])
                                             .AppendFormat("{0:X2}", hash[2])
                                             .AppendFormat("{0:X2}", hash[3]);
            }
 
            var directoryPath = Path.Combine(baseDirectory, directoryName.ToString());
#if CI_BUILD
            if (!allowCopyIfPresent && Directory.Exists(directoryPath))
            {
                // Arcade test retry reuses the machine so the directory might already be present in CI
                int suffix = 1;
                string newDirectoryPath;
                do
                {
                    newDirectoryPath = $"{directoryPath}_{suffix}";
                    suffix++;
                } while (Directory.Exists(newDirectoryPath));
 
                directoryPath = newDirectoryPath;
            }
#endif
 
            return directoryPath;
        }
    }
}