Rename Microsoft.Diagnostics.TestHelpers (#366)
authorMike McLaughlin <mikem@microsoft.com>
Thu, 27 Jun 2019 19:59:46 +0000 (12:59 -0700)
committerGitHub <noreply@github.com>
Thu, 27 Jun 2019 19:59:46 +0000 (12:59 -0700)
60 files changed:
diagnostics.sln
src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/AssertX.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj [deleted file]
src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/TestStep.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs [deleted file]
src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt [deleted file]
src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/AssertX.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/TestStep.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt [new file with mode: 0644]
src/SOS/SOS.UnitTests/SOS.UnitTests.csproj
src/SOS/SOS.UnitTests/SOS.cs
src/SOS/SOS.UnitTests/SOSRunner.cs

index 442e06fb97838ca46e5a43fd5cf2ad2058d168e2..d561e7a35e79a590b748e990f0e1c84eb91f592e 100644 (file)
@@ -1,13 +1,13 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27004.2005
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29019.234
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.NETCore", "src\SOS\SOS.NETCore\SOS.NETCore.csproj", "{20513BA2-A156-4A17-4C70-5AC2DBD4F833}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDebuggee", "src\SOS\lldbplugin.tests\TestDebuggee\TestDebuggee.csproj", "{6C43BE85-F8C3-4D76-8050-F25CE953A7FD}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostic.TestHelpers", "src\Microsoft.Diagnostic.TestHelpers\Microsoft.Diagnostic.TestHelpers.csproj", "{730C1201-1848-4F1E-8C1F-6316FB886C35}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.TestHelpers", "src\Microsoft.Diagnostics.TestHelpers\Microsoft.Diagnostics.TestHelpers.csproj", "{730C1201-1848-4F1E-8C1F-6316FB886C35}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SOS", "SOS", "{41638A4C-0DAF-47ED-A774-ECBBAC0315D7}"
 EndProject
@@ -43,7 +43,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-counters", "src\Tool
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Tools.RuntimeClient", "src\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj", "{54C240C5-7932-4421-A5FB-75205DE0B824}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreateVersionFile", "eng\CreateVersionFile.csproj", "{54E3BCB7-6094-4B25-AC44-D4F914438F03}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateVersionFile", "eng\CreateVersionFile.csproj", "{54E3BCB7-6094-4B25-AC44-D4F914438F03}"
 EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs
deleted file mode 100644 (file)
index e18cfb8..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-// 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 System;
-using System.IO;
-using System.IO.Compression;
-using System.Net;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// Acquires the CLI tools from a web endpoint, a local zip/tar.gz, or directly from a local path
-    /// </summary>
-    public class AcquireDotNetTestStep : TestStep
-    {
-        /// <summary>
-        /// Create a new AcquireDotNetTestStep
-        /// </summary>
-        /// <param name="remoteDotNetZipPath">
-        /// If non-null, the CLI tools will be downloaded from this web endpoint.
-        /// The path should use an http or https scheme and the remote file should be in .zip or .tar.gz format.
-        /// localDotNetZipPath must also be non-null to indicate where the downloaded archive will be cached</param>
-        /// <param name="localDotNetZipPath">
-        /// If non-null, the location of a .zip or .tar.gz compressed folder containing the CLI tools. This
-        /// must be a local file system or network file system path. 
-        /// localDotNetZipExpandDirPath must also be non-null to indicate where the expanded folder will be
-        /// stored.
-        /// localDotNetTarPath must be non-null if localDotNetZip points to a .tar.gz format archive, in order
-        /// to indicate where the .tar file will be cached</param>
-        /// <param name="localDotNetTarPath">
-        /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar
-        /// file. Otherwise this path is unused.</param>
-        /// <param name="localDotNetZipExpandDirPath">
-        /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the
-        /// archive. Otherwise this path is unused.</param>
-        /// <param name="localDotNetPath">
-        /// The path to the dotnet binary. When the CLI tools are being acquired from a compressed archive
-        /// this will presumably be a path inside the localDotNetZipExpandDirPath directory, otherwise
-        /// it can be any local file system path where the dotnet binary can be found.</param>
-        /// <param name="logFilePath">
-        /// The path where an activity log for this test step should be written.
-        /// </param>
-        /// 
-        public AcquireDotNetTestStep(
-            string remoteDotNetZipPath,
-            string localDotNetZipPath,
-            string localDotNetTarPath,
-            string localDotNetZipExpandDirPath,
-            string localDotNetPath,
-            string logFilePath)
-            : base(logFilePath, "Acquire DotNet Tools")
-        {
-            RemoteDotNetPath = remoteDotNetZipPath;
-            LocalDotNetZipPath = localDotNetZipPath;
-            if (localDotNetZipPath != null && localDotNetZipPath.EndsWith(".tar.gz"))
-            {
-                LocalDotNetTarPath = localDotNetTarPath;
-            }
-            if (localDotNetZipPath != null)
-            {
-                LocalDotNetZipExpandDirPath = localDotNetZipExpandDirPath;
-            }
-            LocalDotNetPath = localDotNetPath;
-        }
-
-        /// <summary>
-        /// If non-null, the CLI tools will be downloaded from this web endpoint.
-        /// The path should use an http or https scheme and the remote file should be in .zip or .tar.gz format.
-        /// </summary>
-        public string RemoteDotNetPath { get; private set; }
-
-        /// <summary>
-        /// If non-null, the location of a .zip or .tar.gz compressed folder containing the CLI tools. This
-        /// is a local file system or network file system path. 
-        /// </summary>
-        public string LocalDotNetZipPath { get; private set; }
-
-        /// <summary>
-        /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar
-        /// file. Otherwise null.
-        /// </summary>
-        public string LocalDotNetTarPath { get; private set; }
-
-        /// <summary>
-        /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the
-        /// archive. Otherwise null.
-        /// </summary>
-        public string LocalDotNetZipExpandDirPath { get; private set; }
-
-        /// <summary>
-        /// The path to the dotnet binary when the test step is complete.
-        /// </summary>
-        public string LocalDotNetPath { get; private set; }
-
-        /// <summary>
-        /// Returns true, if there any actual work to do (like downloading, unziping or untaring).
-        /// </summary>
-        public bool AnyWorkToDo { get { return RemoteDotNetPath != null || LocalDotNetZipPath != null; } }
-
-        async protected override Task DoWork(ITestOutputHelper output)
-        {
-            if (RemoteDotNetPath != null)
-            {
-                await DownloadFile(RemoteDotNetPath, LocalDotNetZipPath, output);
-            }
-            if (LocalDotNetZipPath != null)
-            {
-                if (LocalDotNetZipPath.EndsWith(".zip"))
-                {
-                    await Unzip(LocalDotNetZipPath, LocalDotNetZipExpandDirPath, output);
-                }
-                else if(LocalDotNetZipPath.EndsWith(".tar.gz"))
-                {
-                    await UnGZip(LocalDotNetZipPath, LocalDotNetTarPath, output);
-                    await Untar(LocalDotNetTarPath, LocalDotNetZipExpandDirPath, output);
-                }
-                else
-                {
-                    output.WriteLine("Unsupported compression format: " + LocalDotNetZipPath);
-                    throw new NotSupportedException("Unsupported compression format: " + LocalDotNetZipPath);
-                }
-            }
-            output.WriteLine("Dotnet path: " + LocalDotNetPath);
-            if (!File.Exists(LocalDotNetPath))
-            {
-                throw new FileNotFoundException(LocalDotNetPath + " not found");
-            }
-        }
-
-        async static Task DownloadFile(string remotePath, string localPath, ITestOutputHelper output)
-        {
-            output.WriteLine("Downloading: " + remotePath + " -> " + localPath);
-            Directory.CreateDirectory(Path.GetDirectoryName(localPath));
-            WebRequest request = HttpWebRequest.Create(remotePath);
-            WebResponse response = await request.GetResponseAsync();
-            using (FileStream localZipStream = File.OpenWrite(localPath))
-            {
-                // TODO: restore the CopyToAsync code after System.Net.Http.dll is 
-                // updated to a newer version. The current old version has a bug 
-                // where the copy never finished.
-                // await response.GetResponseStream().CopyToAsync(localZipStream);
-                byte[] buffer = new byte[16 * 1024];
-                long bytesLeft = response.ContentLength;
-
-                while (bytesLeft > 0)
-                {
-                    int read = response.GetResponseStream().Read(buffer, 0, buffer.Length);
-                    if (read == 0)
-                        break;
-                    localZipStream.Write(buffer, 0, read);
-                    bytesLeft -= read;
-                }
-                output.WriteLine("Downloading finished");
-            }
-        }
-
-        async static Task UnGZip(string gzipPath, string expandedFilePath, ITestOutputHelper output)
-        {
-            output.WriteLine("Unziping: " + gzipPath + " -> " + expandedFilePath);
-            using (FileStream gzipStream = File.OpenRead(gzipPath))
-            {
-                using (GZipStream expandedStream = new GZipStream(gzipStream, CompressionMode.Decompress))
-                {
-                    using (FileStream targetFileStream = File.OpenWrite(expandedFilePath))
-                    {
-                        await expandedStream.CopyToAsync(targetFileStream);
-                    }
-                }
-            }
-        }
-
-        async static Task Unzip(string zipPath, string expandedDirPath, ITestOutputHelper output)
-        {
-            output.WriteLine("Unziping: " + zipPath + " -> " + expandedDirPath);
-            using (FileStream zipStream = File.OpenRead(zipPath))
-            {
-                ZipArchive zip = new ZipArchive(zipStream);
-                foreach (ZipArchiveEntry entry in zip.Entries)
-                {
-                    string extractedFilePath = Path.Combine(expandedDirPath, entry.FullName);
-                    Directory.CreateDirectory(Path.GetDirectoryName(extractedFilePath));
-                    using (Stream zipFileStream = entry.Open())
-                    {
-                        using (FileStream extractedFileStream = File.OpenWrite(extractedFilePath))
-                        {
-                            await zipFileStream.CopyToAsync(extractedFileStream);
-                        }
-                    }
-                }
-            }
-        }
-
-        async static Task Untar(string tarPath, string expandedDirPath, ITestOutputHelper output)
-        {
-            Directory.CreateDirectory(expandedDirPath);
-            string tarToolPath = null;
-            if (OS.Kind == OSKind.Linux)
-            {
-                tarToolPath = "/bin/tar";
-            }
-            else if (OS.Kind == OSKind.OSX)
-            {
-                tarToolPath = "/usr/bin/tar";
-            }
-            else
-            {
-                throw new NotSupportedException("Unknown where this OS stores the tar executable");
-            }
-
-            await new ProcessRunner(tarToolPath, "-xf " + tarPath).
-                   WithWorkingDirectory(expandedDirPath).
-                   WithLog(output).
-                   WithExpectedExitCode(0).
-                   Run();
-        }
-
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/AssertX.cs b/src/Microsoft.Diagnostic.TestHelpers/AssertX.cs
deleted file mode 100644 (file)
index 2672ca9..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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 System;
-using System.IO;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public static partial class AssertX
-    {
-        public static void DirectoryExists(string dirDescriptiveName, string dirPath, ITestOutputHelper output)
-        {
-            if (!Directory.Exists(dirPath))
-            {
-                string errorMessage = "Expected " + dirDescriptiveName + " to exist: " + dirPath;
-                output.WriteLine(errorMessage);
-                try
-                {
-                    string parentDir = dirPath;
-                    while (true)
-                    {
-                        if (Directory.Exists(Path.GetDirectoryName(parentDir)))
-                        {
-                            output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir));
-                            break;
-                        }
-                        if (Path.GetDirectoryName(parentDir) == parentDir)
-                        {
-                            output.WriteLine("Also unable to find any parent directory that exists");
-                            break;
-                        }
-                        parentDir = Path.GetDirectoryName(parentDir);
-                    }
-                }
-                catch (Exception e)
-                {
-                    output.WriteLine("Additional error while trying to diagnose missing directory:");
-                    output.WriteLine(e.GetType() + ": " + e.Message);
-                }
-                throw new DirectoryNotFoundException(errorMessage);
-            }
-        }
-
-        public static void FileExists(string fileDescriptiveName, string filePath, ITestOutputHelper output)
-        {
-            if (!File.Exists(filePath))
-            {
-                string errorMessage = "Expected " + fileDescriptiveName + " to exist: " + filePath;
-                output.WriteLine(errorMessage);
-                try
-                {
-                    string parentDir = filePath;
-                    while (true)
-                    {
-                        if (Directory.Exists(Path.GetDirectoryName(parentDir)))
-                        {
-                            output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir));
-                            break;
-                        }
-                        if (Path.GetDirectoryName(parentDir) == parentDir)
-                        {
-                            output.WriteLine("Also unable to find any parent directory that exists");
-                            break;
-                        }
-                        parentDir = Path.GetDirectoryName(parentDir);
-                    }
-                }
-                catch (Exception e)
-                {
-                    output.WriteLine("Additional error while trying to diagnose missing file:");
-                    output.WriteLine(e.GetType() + ": " + e.Message);
-                }
-                throw new FileNotFoundException(errorMessage);
-            }
-        }
-    }
-}
-
-
diff --git a/src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs
deleted file mode 100644 (file)
index af2fa21..0000000
+++ /dev/null
@@ -1,222 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// This compiler acquires the CLI tools and uses them to build debuggees.
-    /// </summary>
-    /// <remarks>
-    /// The build process consists of the following steps:
-    ///   1. Acquire the CLI tools from the CliPath. This generally involves downloading them from the web and unpacking them.
-    ///   2. Create a source directory with the conventions that dotnet expects by copying it from the DebuggeeSourceRoot. Any project.template.json
-    ///      file that is found will be specialized by replacing macros with specific contents. This lets us decide the runtime and dependency versions
-    ///      at test execution time.
-    ///   3. Run dotnet restore in the newly created source directory
-    ///   4. Run dotnet build in the same directory
-    ///   5. Rename the built debuggee dll to an exe (so that it conforms to some historical conventions our tests expect)
-    ///   6. Copy any native dependencies from the DebuggeeNativeLibRoot to the debuggee output directory
-    /// </remarks>
-    public abstract class BaseDebuggeeCompiler : IDebuggeeCompiler
-    {
-        AcquireDotNetTestStep _acquireTask;
-        DotNetBuildDebuggeeTestStep _buildDebuggeeTask;
-
-        /// <summary>
-        /// Creates a new BaseDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build debuggees via dotnet build.
-        /// </summary>
-        /// <param name="config">
-        /// The test configuration that will be used to configure the build. The following configuration options should be set in the config:
-        ///   CliPath                                  The location to get the CLI tools from, either as a .zip/.tar.gz at a web endpoint, a .zip/.tar.gz
-        ///                                            at a local filesystem path, or the dotnet binary at a local filesystem path
-        ///   WorkingDir                               Temporary storage for CLI tools compressed archives downloaded from the internet will be stored here
-        ///   CliCacheRoot                             The final CLI tools will be expanded and cached here
-        ///   DebuggeeSourceRoot                       Debuggee sources and template project file will be retrieved from here
-        ///   DebuggeeNativeLibRoot                    Debuggee native binary dependencies will be retrieved from here
-        ///   DebuggeeBuildRoot                        Debuggee final sources/project file/binary outputs will be placed here
-        ///   BuildProjectRuntime                      The runtime moniker to be built
-        ///   BuildProjectMicrosoftNETCoreAppVersion   The nuget package version of Microsoft.NETCore.App package to build against for debuggees that references this library
-        ///   NugetPackageCacheDir                     The directory where NuGet packages are cached during restore
-        ///   NugetFeeds                               The set of nuget feeds that are used to search for packages during restore
-        /// </param>
-        /// <param name="debuggeeName">
-        ///   The name of the debuggee to be built, from which various build file paths are constructed. Before build it is assumed that:
-        ///     Debuggee sources are located at               config.DebuggeeSourceRoot/debuggeeName/
-        ///     Debuggee native dependencies are located at   config.DebuggeeNativeLibRoot/debuggeeName/
-        ///
-        ///   After the build:
-        ///     Debuggee build outputs will be created at     config.DebuggeeNativeLibRoot/debuggeeName/
-        ///     A log of the build is stored at               config.DebuggeeNativeLibRoot/debuggeeName.txt
-        /// </param>
-        public BaseDebuggeeCompiler(TestConfiguration config, string debuggeeName)
-        {
-            _acquireTask = ConfigureAcquireDotNetTask(config);
-            _buildDebuggeeTask = ConfigureDotNetBuildDebuggeeTask(config, _acquireTask.LocalDotNetPath, config.CliVersion, debuggeeName);
-        }
-
-        async public Task<DebuggeeConfiguration> Execute(ITestOutputHelper output)
-        {
-            if (_acquireTask.AnyWorkToDo)
-            {
-                await _acquireTask.Execute(output);
-            }
-            await _buildDebuggeeTask.Execute(output);
-            return new DebuggeeConfiguration(_buildDebuggeeTask.DebuggeeProjectDirPath,
-                                             _buildDebuggeeTask.DebuggeeBinaryDirPath,
-                                             _buildDebuggeeTask.DebuggeeBinaryExePath ?? _buildDebuggeeTask.DebuggeeBinaryDllPath);
-        }
-
-        public static AcquireDotNetTestStep ConfigureAcquireDotNetTask(TestConfiguration config)
-        {
-            string remoteCliZipPath = null;
-            string localCliZipPath = null;
-            string localCliTarPath = null;
-            string localCliExpandedDirPath = null;
-
-            string dotNetPath = config.CliPath;
-            if (dotNetPath.StartsWith("http:") || dotNetPath.StartsWith("https:"))
-            {
-                remoteCliZipPath = dotNetPath;
-                dotNetPath = Path.Combine(config.WorkingDir, "dotnet_zip", Path.GetFileName(remoteCliZipPath));
-            }
-            if (dotNetPath.EndsWith(".zip") || dotNetPath.EndsWith(".tar.gz"))
-            {
-                localCliZipPath = dotNetPath;
-                string cliVersionDirName = null;
-                if (dotNetPath.EndsWith(".tar.gz"))
-                {
-                    localCliTarPath = localCliZipPath.Substring(0, dotNetPath.Length - 3);
-                    cliVersionDirName = Path.GetFileNameWithoutExtension(localCliTarPath);
-                }
-                else
-                {
-                    cliVersionDirName = Path.GetFileNameWithoutExtension(localCliZipPath);
-                }
-
-                localCliExpandedDirPath = Path.Combine(config.CliCacheRoot, cliVersionDirName);
-                dotNetPath = Path.Combine(localCliExpandedDirPath, OS.Kind == OSKind.Windows ? "dotnet.exe" : "dotnet");
-            }
-            string acquireLogDir = Path.GetDirectoryName(Path.GetDirectoryName(dotNetPath));
-            string acquireLogPath = Path.Combine(acquireLogDir, Path.GetDirectoryName(dotNetPath) + ".acquisition_log.txt");
-            return new AcquireDotNetTestStep(
-                remoteCliZipPath,
-                localCliZipPath,
-                localCliTarPath,
-                localCliExpandedDirPath,
-                dotNetPath,
-                acquireLogPath);
-        }
-
-
-        protected static string GetInitialSourceDirPath(TestConfiguration config, string debuggeeName)
-        {
-            return Path.Combine(config.DebuggeeSourceRoot, debuggeeName);
-        }
-
-        protected static string GetDebuggeeNativeLibDirPath(TestConfiguration config, string debuggeeName)
-        {
-            return Path.Combine(config.DebuggeeNativeLibRoot, debuggeeName);
-        }
-
-        protected static string GetDebuggeeSolutionDirPath(string dotNetRootBuildDirPath, string debuggeeName)
-        {
-            return Path.Combine(dotNetRootBuildDirPath, debuggeeName);
-        }
-
-        protected static string GetDotNetRootBuildDirPath(TestConfiguration config)
-        {
-            return config.DebuggeeBuildRoot;
-        }
-
-        protected static string GetDebuggeeProjectDirPath(string debuggeeSolutionDirPath, string initialSourceDirPath, string debuggeeName)
-        {
-            string debuggeeProjectDirPath = debuggeeSolutionDirPath;
-            if (Directory.Exists(Path.Combine(initialSourceDirPath, debuggeeName)))
-            {
-                debuggeeProjectDirPath = Path.Combine(debuggeeSolutionDirPath, debuggeeName);
-            }
-            return debuggeeProjectDirPath;
-        }
-
-        protected virtual string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime)
-        {
-            string debuggeeBinaryDirPath = null;
-            if (runtime != null)
-            {
-                debuggeeBinaryDirPath = Path.Combine(debuggeeProjectDirPath, "bin", "Debug", framework, runtime);
-            }
-            else
-            {
-                debuggeeBinaryDirPath = Path.Combine(debuggeeProjectDirPath, "bin", "Debug", framework);
-            }
-            return debuggeeBinaryDirPath;
-        }
-
-        protected static string GetDebuggeeBinaryDllPath(string debuggeeBinaryDirPath, string debuggeeName)
-        {
-            return Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".dll");
-        }
-
-        protected static string GetDebuggeeBinaryExePath(string debuggeeBinaryDirPath, string debuggeeName)
-        {
-            return Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".exe");
-        }
-
-        protected static string GetLogPath(TestConfiguration config, string framework, string runtime, string debuggeeName)
-        {
-            string version = config.BuildProjectMicrosoftNetCoreAppVersion;
-            return Path.Combine(GetDotNetRootBuildDirPath(config), $"{framework}-{runtime ?? "any"}-{debuggeeName}.txt");
-        }
-
-        protected static Dictionary<string, string> GetNugetFeeds(TestConfiguration config)
-        {
-            Dictionary<string, string> nugetFeeds = new Dictionary<string, string>();
-            if(!string.IsNullOrWhiteSpace(config.NuGetPackageFeeds))
-            {
-                string[] feeds = config.NuGetPackageFeeds.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
-                foreach(string feed in feeds)
-                {
-                    string[] feedParts = feed.Trim().Split('=');
-                    if(feedParts.Length != 2)
-                    {
-                        throw new Exception("Expected feed \'" + feed + "\' to value <key>=<value> format");
-                    }
-                    nugetFeeds.Add(feedParts[0], feedParts[1]);
-                }
-            }
-            return nugetFeeds;
-        }
-
-        protected static string GetRuntime(TestConfiguration config)
-        {
-            return config.BuildProjectRuntime;
-        }
-
-        protected abstract string GetFramework(TestConfiguration config);
-
-        //we anticipate source paths like this:
-        //InitialSource:        <DebuggeeSourceRoot>/<DebuggeeName>
-        //DebuggeeNativeLibDir: <DebuggeeNativeLibRoot>/<DebuggeeName>
-        //DotNetRootBuildDir:   <DebuggeeBuildRoot>
-        //DebuggeeSolutionDir:  <DebuggeeBuildRoot>/<DebuggeeName>
-        //DebuggeeProjectDir:   <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]
-        //DebuggeeBinaryDir:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>]
-        //DebuggeeBinaryDll:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/<DebuggeeName>.dll
-        //DebuggeeBinaryExe:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>]/<DebuggeeName>.exe
-        //LogPath:              <DebuggeeBuildRoot>/<DebuggeeName>.txt
-
-        // When the runtime directory is present it will have a native host exe in it that has been renamed to the debugee
-        // name. It also has a managed dll in it which functions as a managed exe when renamed.
-        // When the runtime directory is missing, the framework directory will have a managed dll in it that functions if it
-        // is renamed to an exe. I'm sure that renaming isn't the intended usage, but it works and produces less churn
-        // in our tests for the moment.
-        public abstract DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName);
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs
deleted file mode 100644 (file)
index 51f619f..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-// 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 System.Collections.Generic;
-using System.IO;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish.
-    /// </summary>
-    public class CliDebuggeeCompiler : BaseDebuggeeCompiler
-    {
-        /// <summary>
-        /// Creates a new CliDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish.
-               /// <param name="config">
-               ///   LinkerPackageVersion   If set, this version of the linker package will be used to link the debuggee during publish.
-               /// </param>
-        /// </summary>
-        public CliDebuggeeCompiler(TestConfiguration config, string debuggeeName) : base(config, debuggeeName) {}
-
-        private static Dictionary<string,string> GetBuildProperties(TestConfiguration config, string runtimeIdentifier)
-        {
-            Dictionary<string, string> buildProperties = new Dictionary<string, string>();
-            buildProperties.Add("RuntimeFrameworkVersion", config.BuildProjectMicrosoftNetCoreAppVersion);
-            buildProperties.Add("BuildProjectFramework", config.BuildProjectFramework);
-            if (runtimeIdentifier != null)
-            {
-                buildProperties.Add("RuntimeIdentifier", runtimeIdentifier);
-            }
-            string debugType = config.DebugType;
-            if (debugType == null)
-            {
-                // The default PDB type is portable
-                debugType = "portable";
-            }
-            buildProperties.Add("DebugType", debugType);
-            return buildProperties;
-        }
-
-        protected override string GetFramework(TestConfiguration config)
-        {
-            return config.BuildProjectFramework ?? "netcoreapp2.0";
-        }
-
-        protected override string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime)
-        {
-            string debuggeeBinaryDirPath = base.GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtime);
-            debuggeeBinaryDirPath = Path.Combine(debuggeeBinaryDirPath, "publish");
-            return debuggeeBinaryDirPath;
-        }
-
-        public override DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName)
-        {
-            string runtimeIdentifier = GetRuntime(config);
-            string framework = GetFramework(config);
-            string initialSourceDirPath = GetInitialSourceDirPath(config, debuggeeName);
-            string dotNetRootBuildDirPath = GetDotNetRootBuildDirPath(config);
-            string debuggeeSolutionDirPath = GetDebuggeeSolutionDirPath(dotNetRootBuildDirPath, debuggeeName);
-            string debuggeeProjectDirPath = GetDebuggeeProjectDirPath(debuggeeSolutionDirPath, initialSourceDirPath, debuggeeName);
-            string debuggeeBinaryDirPath = GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtimeIdentifier);
-            string debuggeeBinaryDllPath = config.IsNETCore ? GetDebuggeeBinaryDllPath(debuggeeBinaryDirPath, debuggeeName) : null;
-            string debuggeeBinaryExePath = config.IsDesktop ? GetDebuggeeBinaryExePath(debuggeeBinaryDirPath, debuggeeName) : null;
-            string logPath = GetLogPath(config, framework, runtimeIdentifier, debuggeeName);
-            return new CsprojBuildDebuggeeTestStep(dotNetPath,
-                                               initialSourceDirPath,
-                                               GetDebuggeeNativeLibDirPath(config, debuggeeName),
-                                               GetBuildProperties(config, runtimeIdentifier),
-                                               runtimeIdentifier,
-                                               config.LinkerPackageVersion,
-                                               debuggeeName,
-                                               debuggeeSolutionDirPath,
-                                               debuggeeProjectDirPath,
-                                               debuggeeBinaryDirPath,
-                                               debuggeeBinaryDllPath,
-                                               debuggeeBinaryExePath,
-                                               config.NuGetPackageCacheDir,
-                                               GetNugetFeeds(config),
-                                               logPath);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs
deleted file mode 100644 (file)
index 38b5af9..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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 System;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public class ConsoleTestOutputHelper : ITestOutputHelper
-    {
-        public void WriteLine(string message)
-        {
-            Console.WriteLine(message);
-        }
-
-        public void WriteLine(string format, params object[] args)
-        {
-            Console.WriteLine(format, args);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs
deleted file mode 100644 (file)
index 914189e..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-// 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 System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using System.Xml.Linq;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// This test step builds debuggees using the dotnet tools with .csproj projects files.
-    /// </summary>
-    /// <remarks>
-    /// Any <debuggee name>.csproj file that is found will be specialized by adding a linker package reference.
-    /// This lets us decide the runtime and dependency versions at test execution time.
-    /// </remarks>
-    public class CsprojBuildDebuggeeTestStep : DotNetBuildDebuggeeTestStep
-    {
-        /// <param name="buildProperties">
-        /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee.
-        /// </param>
-        /// <param name="runtimeIdentifier">
-        /// The runtime moniker to be built.
-        /// </param>
-        public CsprojBuildDebuggeeTestStep(string dotnetToolPath,
-                                       string templateSolutionDirPath,
-                                       string debuggeeNativeLibDirPath,
-                                       Dictionary<string,string> buildProperties,
-                                       string runtimeIdentifier,
-                                       string linkerPackageVersion,
-                                                          string debuggeeName,
-                                       string debuggeeSolutionDirPath,
-                                       string debuggeeProjectDirPath,
-                                       string debuggeeBinaryDirPath,
-                                       string debuggeeBinaryDllPath,
-                                       string debuggeeBinaryExePath,
-                                       string nugetPackageCacheDirPath,
-                                       Dictionary<string,string> nugetFeeds,
-                                       string logPath) :
-            base(dotnetToolPath,
-                 templateSolutionDirPath,
-                 debuggeeNativeLibDirPath,
-                 debuggeeSolutionDirPath,
-                 debuggeeProjectDirPath,
-                 debuggeeBinaryDirPath,
-                 debuggeeBinaryDllPath,
-                 debuggeeBinaryExePath,
-                 nugetPackageCacheDirPath,
-                 nugetFeeds,
-                 logPath)
-        {
-            BuildProperties = buildProperties;
-            RuntimeIdentifier = runtimeIdentifier;
-            DebuggeeName = debuggeeName;
-            ProjectTemplateFileName = debuggeeName + ".csproj";
-            LinkerPackageVersion = linkerPackageVersion;
-        }
-
-        /// <summary>
-        /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee.
-        /// </summary>
-        public IDictionary<string,string> BuildProperties { get; }
-        public string RuntimeIdentifier { get; }
-        public string DebuggeeName { get; }
-        public string LinkerPackageVersion { get; }
-        public override string ProjectTemplateFileName { get; }
-
-        protected override async Task Restore(ITestOutputHelper output)
-        {
-            string extraArgs = "";
-            if (RuntimeIdentifier != null)
-            {
-                extraArgs = " --runtime " + RuntimeIdentifier;
-            }
-            foreach (var prop in BuildProperties)
-            {
-                extraArgs += $" /p:{prop.Key}={prop.Value}";
-            }
-            await Restore(extraArgs, output);
-        }
-
-        protected override async Task Build(ITestOutputHelper output)
-        {
-            string publishArgs = "publish";
-            if (RuntimeIdentifier != null)
-            {
-                publishArgs += " --runtime " + RuntimeIdentifier;
-            }
-            foreach (var prop in BuildProperties)
-            {
-                publishArgs += $" /p:{prop.Key}={prop.Value}";
-            }
-            await Build(publishArgs, output);
-        }
-
-        protected override void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output)
-        {
-            ConvertCsprojTemplate(filePath, Path.Combine(destDirPath, DebuggeeName + ".csproj"));
-        }
-
-        private void ConvertCsprojTemplate(string csprojTemplatePath, string csprojOutPath)
-        {
-            var xdoc = XDocument.Load(csprojTemplatePath);
-            var ns = xdoc.Root.GetDefaultNamespace();
-            if (LinkerPackageVersion != null)
-            {
-                AddLinkerPackageReference(xdoc, ns, LinkerPackageVersion);
-            }
-            using (var fs = new FileStream(csprojOutPath, FileMode.Create))
-            {
-                xdoc.Save(fs);
-            }
-        }
-
-        private static void AddLinkerPackageReference(XDocument xdoc, XNamespace ns, string linkerPackageVersion)
-        {
-            xdoc.Root.Add(new XElement(ns + "ItemGroup",
-                                       new XElement(ns + "PackageReference",
-                                                    new XAttribute("Include", "ILLink.Tasks"),
-                                                    new XAttribute("Version", linkerPackageVersion))));
-        }
-
-        protected override void AssertDebuggeeAssetsFileExists(ITestOutputHelper output)
-        {
-            AssertX.FileExists("debuggee project.assets.json", Path.Combine(DebuggeeProjectDirPath, "obj", "project.assets.json"), output);
-        }
-
-        protected override void AssertDebuggeeProjectFileExists(ITestOutputHelper output)
-        {
-            AssertX.FileExists("debuggee csproj", Path.Combine(DebuggeeProjectDirPath, DebuggeeName + ".csproj"), output);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs
deleted file mode 100644 (file)
index 725bca9..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-// 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 System;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// DebugeeCompiler is responsible for finding and/or producing the source and binaries of a given debuggee.
-    /// The steps it takes to do this depend on the TestConfiguration.
-    /// </summary>
-    public static class DebuggeeCompiler
-    {
-        async public static Task<DebuggeeConfiguration> Execute(TestConfiguration config, string debuggeeName, ITestOutputHelper output)
-        {
-            IDebuggeeCompiler compiler = null;
-            if (config.DebuggeeBuildProcess == "prebuilt")
-            {
-                compiler = new PrebuiltDebuggeeCompiler(config, debuggeeName);
-            }
-            else if (config.DebuggeeBuildProcess == "cli")
-            {
-                compiler = new CliDebuggeeCompiler(config, debuggeeName);
-            }
-            else
-            {
-                throw new Exception("Invalid DebuggeeBuildProcess configuration value. Expected 'prebuilt', actual \'" + config.DebuggeeBuildProcess + "\'");
-            }
-
-            return await compiler.Execute(output);
-        }
-    }
-
-    public interface IDebuggeeCompiler
-    {
-        Task<DebuggeeConfiguration> Execute(ITestOutputHelper output);
-    }
-
-    public class DebuggeeConfiguration
-    {
-        public DebuggeeConfiguration(string sourcePath, string binaryDirPath, string binaryExePath)
-        {
-            SourcePath = sourcePath;
-            BinaryDirPath = binaryDirPath;
-            BinaryExePath = binaryExePath;
-        }
-        public string SourcePath { get; private set; }
-        public string BinaryDirPath { get; private set; }
-        public string BinaryExePath { get; private set; }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs
deleted file mode 100644 (file)
index 7002870..0000000
+++ /dev/null
@@ -1,363 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// This test step builds debuggees using the dotnet tools
-    /// </summary>
-    /// <remarks>
-    /// The build process consists of the following steps:
-    ///   1. Create a source directory with the conventions that dotnet expects by copying it from the DebuggeeSourceRoot. Any template project
-    ///      file that is found will be specialized by the implementation class.
-    ///   2. Run dotnet restore in the newly created source directory
-    ///   3. Run dotnet build in the same directory
-    ///   4. Rename the built debuggee dll to an exe (so that it conforms to some historical conventions our tests expect)
-    ///   5. Copy any native dependencies from the DebuggeeNativeLibRoot to the debuggee output directory
-    /// </remarks>
-    public abstract class DotNetBuildDebuggeeTestStep : TestStep
-    {
-        /// <summary>
-        /// Create a new DotNetBuildDebuggeeTestStep.
-        /// </summary>
-        /// <param name="dotnetToolPath">
-        /// The path to the dotnet executable
-        /// </param>
-        /// <param name="templateSolutionDirPath">
-        /// The path to the template solution source. This will be copied into the final solution source directory
-        /// under debuggeeSolutionDirPath and any project.template.json files it contains will be specialized.
-        /// </param>
-        /// <param name="debuggeeNativeLibDirPath">
-        /// The path where the debuggee's native binary dependencies will be copied from.
-        /// </param>
-        /// <param name="debuggeeSolutionDirPath">
-        /// The path where the debuggee solution will be created. For single project solutions this will be identical to
-        /// the debuggee project directory.
-        /// </param>
-        /// <param name="debuggeeProjectDirPath">
-        /// The path where the primary debuggee executable project directory will be created. For single project solutions this
-        /// will be identical to the debuggee solution directory.
-        /// </param>
-        /// <param name="debuggeeBinaryDirPath">
-        /// The directory path where the dotnet tool will place the compiled debuggee binaries.
-        /// </param>
-        /// <param name="debuggeeBinaryDllPath">
-        /// The path where the dotnet tool will place the compiled debuggee assembly.
-        /// </param>
-        /// <param name="debuggeeBinaryExePath">
-        /// The path to which the build will copy the debuggee binary dll with a .exe extension.
-        /// </param>
-        /// <param name="nugetPackageCacheDirPath">
-        /// The path to the NuGet package cache. If null, no value for this setting will be placed in the
-        /// NuGet.config file and dotnet will need to read it from other ambient NuGet.config files or infer
-        /// a default cache.
-        /// </param>
-        /// <param name="nugetFeeds">
-        /// A mapping of nuget feed names to locations. These feeds will be used to restore debuggee
-        /// nuget package dependencies.
-        /// </param>
-        /// <param name="logPath">
-        /// The path where the build output will be logged
-        /// </param>
-        public DotNetBuildDebuggeeTestStep(string dotnetToolPath,
-                                       string templateSolutionDirPath,
-                                       string debuggeeNativeLibDirPath,
-                                       string debuggeeSolutionDirPath,
-                                       string debuggeeProjectDirPath,
-                                       string debuggeeBinaryDirPath,
-                                       string debuggeeBinaryDllPath,
-                                       string debuggeeBinaryExePath,
-                                       string nugetPackageCacheDirPath,
-                                       Dictionary<string,string> nugetFeeds,
-                                       string logPath) :
-            base(logPath, "Build Debuggee") 
-        {
-            DotNetToolPath = dotnetToolPath;
-            DebuggeeTemplateSolutionDirPath = templateSolutionDirPath;
-            DebuggeeNativeLibDirPath = debuggeeNativeLibDirPath;
-            DebuggeeSolutionDirPath = debuggeeSolutionDirPath;
-            DebuggeeProjectDirPath = debuggeeProjectDirPath;
-            DebuggeeBinaryDirPath = debuggeeBinaryDirPath;
-            DebuggeeBinaryDllPath = debuggeeBinaryDllPath;
-            DebuggeeBinaryExePath = debuggeeBinaryExePath;
-            NuGetPackageCacheDirPath = nugetPackageCacheDirPath;
-            NugetFeeds = nugetFeeds;
-            if(NugetFeeds != null && NugetFeeds.Count > 0)
-            {
-                NuGetConfigPath = Path.Combine(DebuggeeSolutionDirPath, "NuGet.config");
-            }
-        }
-
-        /// <summary>
-        /// The path to the dotnet executable
-        /// </summary>
-        public string DotNetToolPath { get; private set; }
-        /// <summary>
-        /// The path to the template solution source. This will be copied into the final solution source directory
-        /// under debuggeeSolutionDirPath and any project.template.json files it contains will be specialized.
-        /// </summary>
-        public string DebuggeeTemplateSolutionDirPath { get; private set; }
-        /// <summary>
-        /// The path where the debuggee's native binary dependencies will be copied from.
-        /// </summary>
-        public string DebuggeeNativeLibDirPath { get; private set; }
-        /// <summary>
-        /// The path where the debuggee solution will be created. For single project solutions this will be identical to
-        /// the debuggee project directory.
-        /// </summary>
-        public string DebuggeeSolutionDirPath { get; private set; }
-        /// <summary>
-        /// The path where the primary debuggee executable project directory will be created. For single project solutions this
-        /// will be identical to the debuggee solution directory.
-        /// </summary>
-        public string DebuggeeProjectDirPath { get; private set; }
-        /// <summary>
-        /// The directory path where the dotnet tool will place the compiled debuggee binaries.
-        /// </summary>
-        public string DebuggeeBinaryDirPath { get; private set; }
-        /// <summary>
-        /// The path where the dotnet tool will place the compiled debuggee assembly.
-        /// </summary>
-        public string DebuggeeBinaryDllPath { get; private set; }
-        /// <summary>
-        /// The path to which the build will copy the debuggee binary dll with a .exe extension.
-        /// </summary>
-        public string DebuggeeBinaryExePath { get; private set; }
-        /// The path to the NuGet package cache. If null, no value for this setting will be placed in the
-        /// NuGet.config file and dotnet will need to read it from other ambient NuGet.config files or infer
-        /// a default cache.
-        public string NuGetPackageCacheDirPath { get; private set; }
-        public string NuGetConfigPath { get; private set; }
-        public IDictionary<string,string> NugetFeeds { get; private set; }
-        public abstract string ProjectTemplateFileName { get; }
-
-        async protected override Task DoWork(ITestOutputHelper output)
-        {
-            PrepareProjectSolution(output);
-            await Restore(output);
-            await Build(output);
-            CopyNativeDependencies(output);
-        }
-
-        void PrepareProjectSolution(ITestOutputHelper output)
-        {
-            AssertDebuggeeSolutionTemplateDirExists(output);
-
-            output.WriteLine("Creating Solution Source Directory");
-            output.WriteLine("{");
-            IndentedTestOutputHelper indentedOutput = new IndentedTestOutputHelper(output);
-            CopySourceDirectory(DebuggeeTemplateSolutionDirPath, DebuggeeSolutionDirPath, indentedOutput);
-            CreateNuGetConfig(indentedOutput);
-            output.WriteLine("}");
-            output.WriteLine("");
-
-            AssertDebuggeeSolutionDirExists(output);
-            AssertDebuggeeProjectDirExists(output);
-            AssertDebuggeeProjectFileExists(output);
-        }
-
-        SemaphoreSlim _dotnetRestoreLock = new SemaphoreSlim(1);
-
-        protected async Task Restore(string extraArgs, ITestOutputHelper output)
-        {
-            AssertDebuggeeSolutionDirExists(output);
-            AssertDebuggeeProjectDirExists(output);
-            AssertDebuggeeProjectFileExists(output);
-
-            string args = "restore";
-            if (NuGetConfigPath != null)
-            {
-                args += " --configfile " + NuGetConfigPath;
-            }
-            if (NuGetPackageCacheDirPath != null)
-            {
-                args += " --packages \"" + NuGetPackageCacheDirPath + "\"";
-            }
-            if (extraArgs != null)
-            {
-                args += extraArgs;
-            }
-            ProcessRunner runner = new ProcessRunner(DotNetToolPath, args).
-                      WithWorkingDirectory(DebuggeeSolutionDirPath).
-                      WithLog(output).
-                      WithTimeout(TimeSpan.FromMinutes(10)).                    // restore can be painfully slow
-                      WithExpectedExitCode(0);
-
-            if (OS.Kind != OSKind.Windows && Environment.GetEnvironmentVariable("HOME") == null)
-            {
-                output.WriteLine("Detected HOME environment variable doesn't exist. This will trigger a bug in dotnet restore.");
-                output.WriteLine("See: https://github.com/NuGet/Home/issues/2960");
-                output.WriteLine("Test will workaround this by manually setting a HOME value");
-                output.WriteLine("");
-                runner = runner.WithEnvironmentVariable("HOME", DebuggeeSolutionDirPath);
-            }
-
-            //workaround for https://github.com/dotnet/cli/issues/3868
-            await _dotnetRestoreLock.WaitAsync();
-            try
-            {
-                await runner.Run();
-            }
-            finally
-            {
-                _dotnetRestoreLock.Release();
-            }
-
-            AssertDebuggeeAssetsFileExists(output);
-        }
-
-        protected virtual async Task Restore(ITestOutputHelper output)
-        {
-            await Restore(null, output);
-        }
-
-        protected async Task Build(string dotnetArgs, ITestOutputHelper output)
-        {
-            AssertDebuggeeSolutionDirExists(output);
-            AssertDebuggeeProjectFileExists(output);
-            AssertDebuggeeAssetsFileExists(output);
-
-            ProcessRunner runner = new ProcessRunner(DotNetToolPath, dotnetArgs).
-                      WithWorkingDirectory(DebuggeeProjectDirPath).
-                      WithLog(output).
-                      WithTimeout(TimeSpan.FromMinutes(10)). // a mac CI build of the modules debuggee is painfully slow :(
-                      WithExpectedExitCode(0);
-
-            if (OS.Kind != OSKind.Windows && Environment.GetEnvironmentVariable("HOME") == null)
-            {
-                output.WriteLine("Detected HOME environment variable doesn't exist. This will trigger a bug in dotnet build.");
-                output.WriteLine("See: https://github.com/NuGet/Home/issues/2960");
-                output.WriteLine("Test will workaround this by manually setting a HOME value");
-                output.WriteLine("");
-                runner = runner.WithEnvironmentVariable("HOME", DebuggeeSolutionDirPath);
-            }
-            if (NuGetPackageCacheDirPath != null)
-            {
-                //dotnet restore helpfully documents its --packages argument in the help text, but
-                //NUGET_PACKAGES was undocumented as far as I noticed. If this stops working we can also
-                //auto-generate a global.json with a "packages" setting, but this was more expedient.
-                runner = runner.WithEnvironmentVariable("NUGET_PACKAGES", NuGetPackageCacheDirPath);
-            }
-
-            await runner.Run();
-
-            if (DebuggeeBinaryExePath != null)
-            {
-                AssertDebuggeeExeExists(output);
-            }
-            else
-            {
-                AssertDebuggeeDllExists(output);
-            }
-        }
-
-        protected virtual async Task Build(ITestOutputHelper output)
-        {
-            await Build("build", output);
-        }
-
-        void CopyNativeDependencies(ITestOutputHelper output)
-        {
-            if (Directory.Exists(DebuggeeNativeLibDirPath))
-            {
-                foreach (string filePath in Directory.EnumerateFiles(DebuggeeNativeLibDirPath))
-                {
-                    string targetPath = Path.Combine(DebuggeeBinaryDirPath, Path.GetFileName(filePath));
-                    output.WriteLine("Copying: " + filePath + " -> " + targetPath);
-                    File.Copy(filePath, targetPath);
-                }
-            }
-        }
-
-        private void CopySourceDirectory(string sourceDirPath, string destDirPath, ITestOutputHelper output)
-        {
-            output.WriteLine("Copying: " + sourceDirPath + " -> " + destDirPath);
-            Directory.CreateDirectory(destDirPath);
-            foreach(string dirPath in Directory.EnumerateDirectories(sourceDirPath))
-            {
-                CopySourceDirectory(dirPath, Path.Combine(destDirPath, Path.GetFileName(dirPath)), output);
-            }
-            foreach (string filePath in Directory.EnumerateFiles(sourceDirPath))
-            {
-                string fileName = Path.GetFileName(filePath);
-                if (fileName == ProjectTemplateFileName)
-                {
-                    ExpandProjectTemplate(filePath, destDirPath, output);
-                }
-                else
-                {
-                    File.Copy(filePath, Path.Combine(destDirPath, Path.GetFileName(filePath)), true);
-                }
-            }
-        }
-
-        protected abstract void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output);
-
-        protected void CreateNuGetConfig(ITestOutputHelper output)
-        {
-            if (NuGetConfigPath == null)
-            {
-                return;
-            }
-            string nugetConfigPath = Path.Combine(DebuggeeSolutionDirPath, "NuGet.config");
-            StringBuilder sb = new StringBuilder();
-            sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
-            sb.AppendLine("<configuration>");
-            if(NugetFeeds != null && NugetFeeds.Count > 0)
-            {
-                sb.AppendLine("  <packageSources>");
-                sb.AppendLine("    <clear />");
-                foreach(KeyValuePair<string, string> kv in NugetFeeds)
-                {
-                    sb.AppendLine("    <add key=\"" + kv.Key + "\" value=\"" + kv.Value + "\" />");
-                }
-                sb.AppendLine("  </packageSources>");
-                sb.AppendLine("  <activePackageSource>");
-                sb.AppendLine("    <add key=\"All\" value=\"(Aggregate source)\" />");
-                sb.AppendLine("  </activePackageSource>");
-            }
-            sb.AppendLine("</configuration>");
-
-            output.WriteLine("Creating: " + NuGetConfigPath);
-            File.WriteAllText(NuGetConfigPath, sb.ToString());
-        }
-
-        protected void AssertDebuggeeSolutionTemplateDirExists(ITestOutputHelper output)
-        {
-            AssertX.DirectoryExists("debuggee solution template directory", DebuggeeTemplateSolutionDirPath, output);
-        }
-
-        protected void AssertDebuggeeProjectDirExists(ITestOutputHelper output)
-        {
-            AssertX.DirectoryExists("debuggee project directory", DebuggeeProjectDirPath, output);
-        }
-
-        protected void AssertDebuggeeSolutionDirExists(ITestOutputHelper output)
-        {
-            AssertX.DirectoryExists("debuggee solution directory", DebuggeeSolutionDirPath, output);
-        }
-
-        protected void AssertDebuggeeDllExists(ITestOutputHelper output)
-        {
-            AssertX.FileExists("debuggee dll", DebuggeeBinaryDllPath, output);
-        }
-
-        protected void AssertDebuggeeExeExists(ITestOutputHelper output)
-        {
-            AssertX.FileExists("debuggee exe", DebuggeeBinaryExePath, output);
-        }
-
-        protected abstract void AssertDebuggeeAssetsFileExists(ITestOutputHelper output);
-
-        protected abstract void AssertDebuggeeProjectFileExists(ITestOutputHelper output);
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs
deleted file mode 100644 (file)
index 58f7c18..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-// 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 System;
-using System.IO;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// An ITestOutputHelper implementation that logs to a file
-    /// </summary>
-    public class FileTestOutputHelper : ITestOutputHelper, IDisposable
-    {
-        readonly StreamWriter _logWriter;
-        readonly object _lock;
-
-        public FileTestOutputHelper(string logFilePath, FileMode fileMode = FileMode.Create)
-        {
-            Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
-            FileStream fs = new FileStream(logFilePath, fileMode);
-            _logWriter = new StreamWriter(fs);
-            _logWriter.AutoFlush = true;
-            _lock = new object();
-        }
-
-        public void WriteLine(string message)
-        {
-            lock (_lock)
-            {
-                _logWriter.WriteLine(message);
-            }
-        }
-
-        public void WriteLine(string format, params object[] args)
-        {
-            lock (_lock)
-            {
-                _logWriter.WriteLine(format, args);
-            }
-        }
-
-        public void Dispose()
-        {
-            lock (_lock)
-            {
-                _logWriter.Dispose();
-            }
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs b/src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs
deleted file mode 100644 (file)
index 7097525..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public enum ProcessStream
-    {
-        StandardIn = 0,
-        StandardOut = 1,
-        StandardError = 2,
-        MaxStreams = 3
-    }
-
-    public enum KillReason
-    {
-        TimedOut,
-        Unknown
-    }
-
-    public interface IProcessLogger
-    {
-        void ProcessExited(ProcessRunner runner);
-        void ProcessKilled(ProcessRunner runner, KillReason reason);
-        void ProcessStarted(ProcessRunner runner);
-        void Write(ProcessRunner runner, string data, ProcessStream stream);
-        void WriteLine(ProcessRunner runner, string data, ProcessStream stream);
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs
deleted file mode 100644 (file)
index 49f3a22..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// An implementation of ITestOutputHelper that adds one indent level to
-    /// the start of each line
-    /// </summary>
-    public class IndentedTestOutputHelper : ITestOutputHelper
-    {
-        readonly string _indentText;
-        readonly ITestOutputHelper _output;
-
-        public IndentedTestOutputHelper(ITestOutputHelper innerOutput, string indentText = "    ")
-        {
-            _output = innerOutput;
-            _indentText = indentText;
-        }
-
-        public void WriteLine(string message)
-        {
-            _output.WriteLine(_indentText + message);
-        }
-
-        public void WriteLine(string format, params object[] args)
-        {
-            _output.WriteLine(_indentText + format, args);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj b/src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj
deleted file mode 100644 (file)
index 0800c15..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<!-- Copyright (c)  Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information. -->
-<Project Sdk="Microsoft.NET.Sdk">
-  <PropertyGroup>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
-    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <NoWarn>;1591;1701</NoWarn>
-    <IsPackable>true</IsPackable>
-    <Description>Diagnostic test support</Description>
-    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
-    <PackageTags>tests</PackageTags>
-    <DebugType>embedded</DebugType>
-  </PropertyGroup>
-  
-  <ItemGroup>
-    <PackageReference Include="xunit" Version="$(XUnitVersion)" />
-    <PackageReference Include="xunit.abstractions" Version="$(XUnitAbstractionsVersion)" />
-  </ItemGroup>
-</Project>
diff --git a/src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs
deleted file mode 100644 (file)
index 781f1d1..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public class MultiplexTestOutputHelper : ITestOutputHelper
-    {
-        readonly ITestOutputHelper[] _outputs;
-
-        public MultiplexTestOutputHelper(params ITestOutputHelper[] outputs)
-        {
-            _outputs = outputs;
-        }
-
-        public void WriteLine(string message)
-        {
-            foreach(ITestOutputHelper output in _outputs)
-            {
-                output.WriteLine(message);
-            }
-        }
-
-        public void WriteLine(string format, params object[] args)
-        {
-            foreach (ITestOutputHelper output in _outputs)
-            {
-                output.WriteLine(format, args);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs
deleted file mode 100644 (file)
index 6e0ae72..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 System.IO;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public class PrebuiltDebuggeeCompiler : IDebuggeeCompiler
-    {
-        string _sourcePath;
-        string _binaryPath;
-        string _binaryExePath;
-
-        public PrebuiltDebuggeeCompiler(TestConfiguration config, string debuggeeName)
-        {
-            //we anticipate paths like this:
-            //Source:   <DebuggeeSourceRoot>/<DebuggeeName>/[<DebuggeeName>]
-            //Binaries: <DebuggeeBuildRoot>/<DebuggeeName>/
-            _sourcePath = Path.Combine(config.DebuggeeSourceRoot, debuggeeName);
-            if (Directory.Exists(Path.Combine(_sourcePath, debuggeeName)))
-            {
-                _sourcePath = Path.Combine(_sourcePath, debuggeeName);
-            }
-
-            _binaryPath = Path.Combine(config.DebuggeeBuildRoot, debuggeeName);
-            _binaryExePath = Path.Combine(_binaryPath, debuggeeName);
-            _binaryExePath += ".exe";
-        }
-
-        public Task<DebuggeeConfiguration> Execute(ITestOutputHelper output)
-        {
-            return Task.Factory.StartNew<DebuggeeConfiguration>(() => new DebuggeeConfiguration(_sourcePath, _binaryPath, _binaryExePath));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs b/src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs
deleted file mode 100644 (file)
index 1e24048..0000000
+++ /dev/null
@@ -1,469 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// Executes a process and logs the output
-    /// </summary>
-    /// <remarks>
-    /// The intended lifecycle is:
-    ///   a) Create a new ProcessRunner
-    ///   b) Use the various WithXXX methods to modify the configuration of the process to launch
-    ///   c) await RunAsync() to start the process and wait for it to terminate. Configuration
-    ///      changes are no longer possible
-    ///   d) While waiting for RunAsync(), optionally call Kill() one or more times. This will expedite 
-    ///      the termination of the process but there is no guarantee the process is terminated by
-    ///      the time Kill() returns.
-    ///      
-    ///   Although the entire API of this type has been designed to be thread-safe, its typical that
-    ///   only calls to Kill() and property getters invoked within the logging callbacks will be called
-    ///   asynchronously.
-    /// </remarks>
-    public class ProcessRunner
-    {
-        // All of the locals might accessed from multiple threads and need to read/written under
-        // the _lock. We also use the lock to synchronize property access on the process object.
-        //
-        // Be careful not to cause deadlocks by calling the logging callbacks with the lock held.
-        // The logger has its own lock and it will hold that lock when it calls into property getters
-        // on this type.
-        object _lock = new object();
-
-        List<IProcessLogger> _loggers;
-        Process _p;
-        DateTime _startTime;
-        TimeSpan _timeout;
-        ITestOutputHelper _traceOutput;
-        int? _expectedExitCode;
-        TaskCompletionSource<Process> _waitForProcessStartTaskSource;
-        Task<int> _waitForExitTask;
-        Task _timeoutProcessTask;
-        Task _readStdOutTask;
-        Task _readStdErrTask;
-        CancellationTokenSource _cancelSource;
-        private string _replayCommand;
-        private KillReason? _killReason;
-
-        public ProcessRunner(string exePath, string arguments, string replayCommand = null)
-        {
-            ProcessStartInfo psi = new ProcessStartInfo();
-            psi.FileName = exePath;
-            psi.Arguments = arguments;
-            psi.UseShellExecute = false;
-            psi.RedirectStandardInput = true;
-            psi.RedirectStandardOutput = true;
-            psi.RedirectStandardError = true;
-            psi.CreateNoWindow = true;
-
-            lock (_lock)
-            {
-                _p = new Process();
-                _p.StartInfo = psi;
-                _p.EnableRaisingEvents = false;
-                _loggers = new List<IProcessLogger>();
-                _timeout = TimeSpan.FromMinutes(10);
-                _cancelSource = new CancellationTokenSource();
-                _killReason = null;
-                _waitForProcessStartTaskSource = new TaskCompletionSource<Process>();
-                Task<Process> startTask = _waitForProcessStartTaskSource.Task;
-                
-                // unfortunately we can't use the default Process stream reading because it only returns full lines and we have scenarios
-                // that need to receive the output before the newline character is written
-                _readStdOutTask = startTask.ContinueWith(t =>
-                {
-                    ReadStreamToLoggers(_p.StandardOutput, ProcessStream.StandardOut, _cancelSource.Token);
-                }, 
-                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
-
-                _readStdErrTask = startTask.ContinueWith(t =>
-                {
-                    ReadStreamToLoggers(_p.StandardError, ProcessStream.StandardError, _cancelSource.Token);
-                }, 
-                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
-
-                _timeoutProcessTask = startTask.ContinueWith(t =>
-                {
-                    Task.Delay(_timeout, _cancelSource.Token).ContinueWith(t2 => Kill(KillReason.TimedOut), TaskContinuationOptions.NotOnCanceled);
-                },
-                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
-
-                _waitForExitTask = InternalWaitForExit(startTask, _readStdOutTask, _readStdErrTask);
-                
-                if (replayCommand == null)
-                {
-                    _replayCommand = ExePath + " " + Arguments;
-                }
-                else
-                {
-                    _replayCommand = replayCommand;
-                }
-            }
-        }
-        
-        public string ReplayCommand
-        {
-            get { lock (_lock) { return _replayCommand; } }
-        }
-
-        public ProcessRunner WithEnvironmentVariable(string key, string value)
-        {
-            lock (_lock)
-            {
-                _p.StartInfo.Environment[key] = value;
-            }
-            return this;
-        }
-
-        public ProcessRunner WithWorkingDirectory(string workingDirectory)
-        {
-            lock (_lock)
-            {
-                _p.StartInfo.WorkingDirectory = workingDirectory;
-            }
-            return this;
-        }
-
-        public ProcessRunner WithLog(IProcessLogger logger)
-        {
-            lock (_lock)
-            {
-                _loggers.Add(logger);
-            }
-            return this;
-        }
-
-        public ProcessRunner WithLog(ITestOutputHelper output)
-        {
-            lock (_lock)
-            {
-                _loggers.Add(new TestOutputProcessLogger(output));
-            }
-            return this;
-        }
-
-        public ProcessRunner WithDiagnosticTracing(ITestOutputHelper traceOutput)
-        {
-            lock (_lock)
-            {
-                _traceOutput = new ConsoleTestOutputHelper(traceOutput);
-            }
-            return this;
-        }
-
-        public IProcessLogger[] Loggers
-        {
-            get { lock (_lock) { return _loggers.ToArray(); } }
-        }
-
-        public ProcessRunner WithTimeout(TimeSpan timeout)
-        {
-            lock (_lock)
-            {
-                _timeout = timeout;
-            }
-            return this;
-        }
-
-        public ProcessRunner WithExpectedExitCode(int expectedExitCode)
-        {
-            lock (_lock)
-            {
-                _expectedExitCode = expectedExitCode;
-            }
-            return this;
-        }
-
-        public string ExePath
-        {
-            get { lock (_lock) { return _p.StartInfo.FileName; } }
-        }
-
-        public string Arguments
-        {
-            get { lock (_lock) { return _p.StartInfo.Arguments; } }
-        }
-
-        public string WorkingDirectory
-        {
-            get { lock (_lock) { return _p.StartInfo.WorkingDirectory; } }
-        }
-
-        public int ProcessId
-        {
-            get { lock (_lock) { return _p.Id; } }
-        }
-
-        public Dictionary<string,string> EnvironmentVariables
-        {
-            get { lock (_lock) { return new Dictionary<string, string>(_p.StartInfo.Environment); } }
-        }
-
-        public bool IsStarted
-        {
-            get { lock (_lock) { return _waitForProcessStartTaskSource.Task.IsCompleted; } }
-        }
-
-        public DateTime StartTime
-        {
-            get { lock (_lock) { return _startTime; } }
-        }
-
-        public int ExitCode
-        {
-            get { lock (_lock) { return _p.ExitCode; } }
-        }
-
-        public void StandardInputWriteLine(string line)
-        {
-            IProcessLogger[] loggers = null;
-            StreamWriter inputStream = null;
-            lock (_lock)
-            {
-                loggers = _loggers.ToArray();
-                inputStream = _p.StandardInput;
-            }
-            foreach (IProcessLogger logger in loggers)
-            {
-                logger.WriteLine(this, line, ProcessStream.StandardIn);
-            }
-            inputStream.WriteLine(line);
-        }
-
-        public Task<int> Run()
-        {
-            Start();
-            return WaitForExit();
-        }
-
-        public Task<int> WaitForExit()
-        {
-            lock (_lock)
-            {
-                return _waitForExitTask;
-            }
-        }
-
-        public ProcessRunner Start()
-        {
-            Process p = null;
-            lock (_lock)
-            {
-                p = _p;
-            }
-            // this is safe to call on multiple threads, it only launches the process once
-            bool started = p.Start();
-
-            IProcessLogger[] loggers = null;
-            lock (_lock)
-            {
-                // only the first thread to get here will initialize this state
-                if (!_waitForProcessStartTaskSource.Task.IsCompleted)
-                {
-                    loggers = _loggers.ToArray();
-                    _startTime = DateTime.Now;
-                    _waitForProcessStartTaskSource.SetResult(_p);
-                }
-            }
-
-            // only the first thread that entered the lock above will run this
-            if (loggers != null)
-            {
-                foreach (IProcessLogger logger in loggers)
-                {
-                    logger.ProcessStarted(this);
-                }
-            }
-
-            return this;
-        }
-
-        private void ReadStreamToLoggers(StreamReader reader, ProcessStream stream, CancellationToken cancelToken)
-        {
-            IProcessLogger[] loggers = Loggers;
-
-            // for the best efficiency we want to read in chunks, but if the underlying stream isn't
-            // going to timeout partial reads then we have to fall back to reading one character at a time
-            int readChunkSize = 1;
-            if (reader.BaseStream.CanTimeout)
-            {
-                readChunkSize = 1000;
-            }
-
-            char[] buffer = new char[readChunkSize];
-            bool lastCharWasCarriageReturn = false;
-            do
-            {
-                int charsRead = 0;
-                int lastStartLine = 0;
-                charsRead = reader.ReadBlock(buffer, 0, readChunkSize);
-
-                // this lock keeps the standard out/error streams from being intermixed
-                lock (loggers)
-                {
-                    for (int i = 0; i < charsRead; i++)
-                    {
-                        // eat the \n after a \r, if any
-                        bool isNewLine = buffer[i] == '\n';
-                        bool isCarriageReturn = buffer[i] == '\r';
-                        if (lastCharWasCarriageReturn && isNewLine)
-                        {
-                            lastStartLine++;
-                            lastCharWasCarriageReturn = false;
-                            continue;
-                        }
-                        lastCharWasCarriageReturn = isCarriageReturn;
-                        if (isCarriageReturn || isNewLine)
-                        {
-                            string line = new string(buffer, lastStartLine, i - lastStartLine);
-                            lastStartLine = i + 1;
-                            foreach (IProcessLogger logger in loggers)
-                            {
-                                logger.WriteLine(this, line, stream);
-                            }
-                        }
-                    }
-
-                    // flush any fractional line
-                    if (charsRead > lastStartLine)
-                    {
-                        string line = new string(buffer, lastStartLine, charsRead - lastStartLine);
-                        foreach (IProcessLogger logger in loggers)
-                        {
-                            logger.Write(this, line, stream);
-                        }
-                    }
-                }
-            }
-            while (!reader.EndOfStream && !cancelToken.IsCancellationRequested);
-        }
-
-        public void Kill(KillReason reason = KillReason.Unknown)
-        {
-            IProcessLogger[] loggers = null;
-            Process p = null;
-            lock (_lock)
-            {
-                if (_waitForExitTask.IsCompleted)
-                {
-                    return;
-                }
-                if (_killReason.HasValue)
-                {
-                    return;
-                }
-                _killReason = reason;
-                if (!_p.HasExited)
-                {
-                    p = _p;
-                }
-
-                loggers = _loggers.ToArray();
-                _cancelSource.Cancel();
-            }
-
-            if (p != null)
-            {
-                // its possible the process could exit just after we check so
-                // we still have to handle the InvalidOperationException that
-                // can be thrown.
-                try
-                {
-                    p.Kill();
-                }
-                catch (InvalidOperationException) { }
-            }
-
-            foreach (IProcessLogger logger in loggers)
-            {
-                logger.ProcessKilled(this, reason);
-            }
-        }
-
-        private async Task<int> InternalWaitForExit(Task<Process> startProcessTask, Task stdOutTask, Task stdErrTask)
-        {
-            DebugTrace("starting InternalWaitForExit");
-            Process p = await startProcessTask;
-            DebugTrace("InternalWaitForExit {0} '{1}'", p.Id, _replayCommand);
-
-            Task processExit = Task.Factory.StartNew(() =>
-            {
-                DebugTrace("starting Process.WaitForExit {0}", p.Id);
-                p.WaitForExit();
-                DebugTrace("ending Process.WaitForExit {0}", p.Id);
-            },
-            TaskCreationOptions.LongRunning);
-
-            DebugTrace("awaiting process {0} exit, stdOut, and stdErr", p.Id);
-            await Task.WhenAll(processExit, stdOutTask, stdErrTask);
-            DebugTrace("await process {0} exit, stdOut, and stdErr complete", p.Id);
-
-            foreach (IProcessLogger logger in Loggers)
-            {
-                logger.ProcessExited(this);
-            }
-
-            lock (_lock)
-            {
-                if (_expectedExitCode.HasValue && p.ExitCode != _expectedExitCode.Value)
-                {
-                    throw new Exception("Process returned exit code " + p.ExitCode + ", expected " + _expectedExitCode.Value + Environment.NewLine +
-                                        "Command Line: " + ReplayCommand + Environment.NewLine +
-                                        "Working Directory: " + WorkingDirectory);
-                }
-                DebugTrace("InternalWaitForExit {0} returning {1}", p.Id, p.ExitCode);
-                return p.ExitCode;
-            }
-        }
-
-        private void DebugTrace(string format, params object[] args)
-        {
-            lock (_lock)
-            {
-                if (_traceOutput != null)
-                {
-                    string message = string.Format(format, args);
-                    _traceOutput.WriteLine("TRACE: {0}", message);
-                }
-            }
-        }
-
-        class ConsoleTestOutputHelper : ITestOutputHelper
-        {
-            readonly ITestOutputHelper _output;
-
-            public ConsoleTestOutputHelper(ITestOutputHelper output)
-            {
-                _output = output;
-            }
-
-            public void WriteLine(string message)
-            {
-                Console.WriteLine(message);
-                if (_output != null)
-                {
-                    _output.WriteLine(message);
-                }
-
-            }
-
-            public void WriteLine(string format, params object[] args)
-            {
-                Console.WriteLine(format, args);
-                if (_output != null)
-                {
-                    _output.WriteLine(format, args);
-                }
-            }
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs b/src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs
deleted file mode 100644 (file)
index 17d5a59..0000000
+++ /dev/null
@@ -1,702 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Xml.Linq;
-using Xunit;
-using Xunit.Extensions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// Represents the all the test configurations for a test run.
-    /// </summary>
-    public class TestRunConfiguration : IDisposable
-    {
-        public static TestRunConfiguration Instance
-        {
-            get { return _instance.Value; }
-        }
-
-        static Lazy<TestRunConfiguration> _instance = new Lazy<TestRunConfiguration>(() => ParseDefaultConfigFile());
-
-        static TestRunConfiguration ParseDefaultConfigFile()
-        {
-            string configFilePath = Path.Combine(TestConfiguration.BaseDir, "Debugger.Tests.Config.txt");
-            TestRunConfiguration testRunConfig = new TestRunConfiguration();
-            testRunConfig.ParseConfigFile(configFilePath);
-            return testRunConfig;
-        }
-
-        DateTime _timestamp = DateTime.Now;
-
-        public IEnumerable<TestConfiguration> Configurations { get; private set; }
-
-        void ParseConfigFile(string path)
-        {
-            string nugetPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
-            if (nugetPackages == null)
-            {
-                // If not already set, the arcade SDK scripts/build system sets NUGET_PACKAGES 
-                // to the UserProfile or HOME nuget cache directories if building locally (for 
-                // speed) or to the repo root/.packages in CI builds (to isolate global machine 
-                // dependences).
-                //
-                // This emulates that logic so the VS Test Explorer can still run the tests for
-                // config files that don't set the NugetPackagesCacheDir value (like the SOS unit
-                // tests).
-                string nugetPackagesRoot = null;
-                if (OS.Kind == OSKind.Windows)
-                {
-                    nugetPackagesRoot = Environment.GetEnvironmentVariable("UserProfile");
-                }
-                else if (OS.Kind == OSKind.Linux || OS.Kind == OSKind.OSX)
-                {
-                    nugetPackagesRoot = Environment.GetEnvironmentVariable("HOME");
-                }
-                if (nugetPackagesRoot != null)
-                {
-                    nugetPackages = Path.Combine(nugetPackagesRoot, ".nuget", "packages");
-                }
-            }
-            // The TargetArchitecture and NuGetPackageCacheDir can still be overridden
-            // in a config file. This is just setting the default. The other values can 
-            // also // be overridden but it is not recommended.
-            Dictionary<string, string> initialConfig = new Dictionary<string, string>
-            {
-                ["Timestamp"] = GetTimeStampText(),
-                ["TempPath"] = Path.GetTempPath(),
-                ["WorkingDir"] = GetInitialWorkingDir(),
-                ["OS"] = OS.Kind.ToString(),
-                ["TargetArchitecture"] = OS.TargetArchitecture.ToString().ToLowerInvariant(),
-                ["NuGetPackageCacheDir"] = nugetPackages
-            };
-            if (OS.Kind == OSKind.Windows)
-            {
-                initialConfig["WinDir"] = Path.GetFullPath(Environment.GetEnvironmentVariable("WINDIR"));
-            }
-            IEnumerable<Dictionary<string, string>> configs = ParseConfigFile(path, new Dictionary<string, string>[] { initialConfig });
-            Configurations = configs.Select(c => new TestConfiguration(c));
-        }
-
-        Dictionary<string, string>[] ParseConfigFile(string path, Dictionary<string, string>[] templates)
-        {
-            XDocument doc = XDocument.Load(path);
-            XElement elem = doc.Root;
-            Assert.Equal("Configuration", elem.Name);
-            return ParseConfigSettings(templates, elem);
-        }
-
-        string GetTimeStampText()
-        {
-            return _timestamp.ToString("yyyy\\_MM\\_dd\\_hh\\_mm\\_ss\\_ffff");
-        }
-
-        string GetInitialWorkingDir()
-        {
-            return Path.Combine(Path.GetTempPath(), "TestRun_" + GetTimeStampText());
-        }
-
-        Dictionary<string, string>[] ParseConfigSettings(Dictionary<string, string>[] templates, XElement node)
-        {
-            Dictionary<string, string>[] currentTemplates = templates;
-            foreach (XElement child in node.Elements())
-            {
-                currentTemplates = ParseConfigSetting(currentTemplates, child);
-            }
-            return currentTemplates;
-        }
-
-        Dictionary<string, string>[] ParseConfigSetting(Dictionary<string, string>[] templates, XElement node)
-        {
-            // As long as the templates are added at the end of the list, the "current" 
-            // config for this section is the last one in the array.
-            Dictionary<string, string> currentTemplate = templates.Last();
-
-            switch (node.Name.LocalName)
-            { 
-                case "Options":
-                    if (EvaluateConditional(currentTemplate, node))
-                    {
-                        List<Dictionary<string, string>> newTemplates = new List<Dictionary<string, string>>();
-                        foreach (XElement optionNode in node.Elements("Option"))
-                        {
-                            if (EvaluateConditional(currentTemplate, optionNode))
-                            {
-                                IEnumerable<Dictionary<string, string>> templateCopy = templates.Select(c => new Dictionary<string, string>(c));
-                                newTemplates.AddRange(ParseConfigSettings(templateCopy.ToArray(), optionNode));
-                            }
-                        }
-                        if (newTemplates.Count > 0)
-                        {
-                            return newTemplates.ToArray();
-                        }
-                    }
-                    break;
-
-                case "Import":
-                    if (EvaluateConditional(currentTemplate, node))
-                    {
-                        foreach (XAttribute attr in node.Attributes("ConfigFile"))
-                        {
-                            string file = ResolveProperties(currentTemplate, attr.Value).Trim();
-                            if (!Path.IsPathRooted(file))
-                            {
-                                file = Path.Combine(TestConfiguration.BaseDir, file);
-                            }
-                            templates = ParseConfigFile(file, templates);
-                        }
-                    }
-                    break;
-
-                default:
-                    foreach (Dictionary<string, string> config in templates)
-                    {
-                        // This checks the condition on an individual config value
-                        if (EvaluateConditional(config, node))
-                        {
-                            string resolveNodeValue = ResolveProperties(config, node.Value);
-                            config[node.Name.LocalName] = resolveNodeValue;
-                        }
-                    }
-                    break;
-            }
-            return templates;
-        }
-
-        bool EvaluateConditional(Dictionary<string, string> config, XElement node)
-        {
-            foreach (XAttribute attr in node.Attributes("Condition"))
-            {
-                string conditionText = attr.Value;
-
-                // Check if Exists('<directory or file>')
-                const string existsKeyword = "Exists('";
-                int existsStartIndex = conditionText.IndexOf(existsKeyword);
-                if (existsStartIndex != -1)
-                {
-                    bool not = (existsStartIndex > 0) && (conditionText[existsStartIndex - 1] == '!');
-
-                    existsStartIndex += existsKeyword.Length;
-                    int existsEndIndex = conditionText.IndexOf("')", existsStartIndex);
-                    Assert.NotEqual(-1, existsEndIndex);
-
-                    string path = conditionText.Substring(existsStartIndex, existsEndIndex - existsStartIndex);
-                    path = Path.GetFullPath(ResolveProperties(config, path));
-                    bool exists = Directory.Exists(path) || File.Exists(path);
-                    return not ? !exists : exists;
-                }
-                else
-                {
-                    // Check if equals and not equals
-                    string[] parts = conditionText.Split("==");
-                    bool equal;
-
-                    if (parts.Length == 2)
-                    {
-                        equal = true;
-                    }
-                    else
-                    {
-                        parts = conditionText.Split("!=");
-                        Assert.Equal(2, parts.Length);
-                        equal = false;
-                    }
-                    // Resolve any config values in the condition
-                    string leftValue = ResolveProperties(config, parts[0]).Trim();
-                    string rightValue = ResolveProperties(config, parts[1]).Trim();
-
-                    // Now do the simple string comparison of the left/right sides of the condition
-                    return equal ? leftValue == rightValue : leftValue != rightValue;
-                }
-            }
-            return true;
-        }
-
-        private string ResolveProperties(Dictionary<string, string> config, string rawNodeValue)
-        {
-            StringBuilder resolvedValue = new StringBuilder();
-            for(int i = 0; i < rawNodeValue.Length; )
-            {
-                int propStartIndex = rawNodeValue.IndexOf("$(", i);
-                if (propStartIndex == -1)
-                {
-                    if (i != rawNodeValue.Length)
-                    {
-                        resolvedValue.Append(rawNodeValue.Substring(i));
-                    }
-                    break;
-                }
-                else
-                {
-                    int propEndIndex = rawNodeValue.IndexOf(")", propStartIndex+1);
-                    Assert.NotEqual(-1, propEndIndex);
-                    if (propStartIndex != i)
-                    {
-                        resolvedValue.Append(rawNodeValue.Substring(i, propStartIndex - i));
-                    }
-                    // Now resolve the property name from the config dictionary
-                    string propertyName = rawNodeValue.Substring(propStartIndex + 2, propEndIndex - propStartIndex - 2);
-                    resolvedValue.Append(config.GetValueOrDefault(propertyName, ""));
-                    i = propEndIndex + 1;
-                }
-            }
-
-            return resolvedValue.ToString();
-        }
-
-        public void Dispose()
-        {
-        }
-    }
-
-    /// <summary>
-    /// Represents the current test configuration
-    /// </summary>
-    public class TestConfiguration
-    {
-        const string DebugTypeKey = "DebugType";
-        const string DebuggeeBuildRootKey = "DebuggeeBuildRoot";
-
-        internal static readonly string BaseDir = Path.GetFullPath(".");
-
-        private Dictionary<string, string> _settings;
-
-        public TestConfiguration()
-        {
-            _settings = new Dictionary<string, string>();
-        }
-
-        public TestConfiguration(Dictionary<string, string> initialSettings)
-        {
-            _settings = new Dictionary<string, string>(initialSettings);
-        }
-
-        public IReadOnlyDictionary<string, string> AllSettings
-        {
-            get { return _settings; }
-        }
-
-        public TestConfiguration CloneWithNewDebugType(string pdbType)
-        {
-            Debug.Assert(!string.IsNullOrWhiteSpace(pdbType));
-
-            var currentSettings = new Dictionary<string, string>(_settings);
-
-            // Set or replace if the pdb debug type
-            currentSettings[DebugTypeKey] = pdbType;
-
-            // The debuggee build root must exist. Append the pdb type to make it unique.
-            currentSettings[DebuggeeBuildRootKey] = Path.Combine(currentSettings[DebuggeeBuildRootKey], pdbType);
-
-            return new TestConfiguration(currentSettings);
-        }
-
-        /// <summary>
-        /// The target architecture (x64, x86, arm, arm64) to build and run. If the config
-        /// file doesn't have an TargetArchitecture property, then the current running
-        /// architecture is used.
-        /// </summary>
-        public string TargetArchitecture
-        {
-            get { return GetValue("TargetArchitecture").ToLowerInvariant(); }
-        }
-
-        /// <summary>
-        /// Built for "Debug" or "Release". Can be null.
-        /// </summary>
-        public string TargetConfiguration
-        {
-            get { return GetValue("TargetConfiguration"); }
-        }
-
-        /// <summary>
-        /// The product "projectk" (.NET Core) or "desktop".
-        /// </summary>
-        public string TestProduct
-        {
-            get { return GetValue("TestProduct").ToLowerInvariant(); }
-        }
-
-        /// <summary>
-        /// Returns true if running on .NET Core (based on TestProduct).
-        /// </summary>
-        public bool IsNETCore
-        {
-            get { return TestProduct.Equals("projectk"); }
-        }
-
-        /// <summary>
-        /// Returns true if running on desktop framework (based on TestProduct).
-        /// </summary>
-        public bool IsDesktop
-        {
-            get { return TestProduct.Equals("desktop"); }
-        }
-
-        /// <summary>
-        /// The test runner script directory 
-        /// </summary>
-        public string ScriptRootDir
-        {
-            get { return MakeCanonicalPath(GetValue("ScriptRootDir")); }
-        }
-
-        /// <summary>
-        /// Working temporary directory.
-        /// </summary>
-        public string WorkingDir
-        {
-            get { return MakeCanonicalPath(GetValue("WorkingDir")); }
-        }
-
-        /// <summary>
-        /// The host program to run a .NET Core or null for desktop/no host.
-        /// </summary>
-        public string HostExe
-        {
-            get { return MakeCanonicalExePath(GetValue("HostExe")); }
-        }
-
-        /// <summary>
-        /// Arguments to the HostExe.
-        /// </summary>
-        public string HostArgs
-        {
-            get { return GetValue("HostArgs"); }
-        }
-
-        /// <summary>
-        /// Environment variables to pass to the target process (via the ProcessRunner).
-        /// </summary>
-        public string HostEnvVars
-        {
-            get { return GetValue("HostEnvVars"); }
-        }
-
-        /// <summary>
-        /// Add the host environment variables to the process runner.
-        /// </summary>
-        /// <param name="runner">process runner instance</param>
-        public void AddHostEnvVars(ProcessRunner runner)
-        {
-            if (HostEnvVars != null)
-            {
-                string[] vars = HostEnvVars.Split(';');
-                foreach (string var in vars)
-                {
-                    if (string.IsNullOrEmpty(var))
-                    {
-                        continue;
-                    }
-                    string[] parts = var.Split('=');
-                    runner = runner.WithEnvironmentVariable(parts[0], parts[1]);
-                }
-            }
-        }
-
-        /// <summary>
-        /// The directory to the runtime (coreclr.dll, etc.) symbols
-        /// </summary>
-        public string RuntimeSymbolsPath
-        {
-            get { return MakeCanonicalPath(GetValue("RuntimeSymbolsPath")); }
-        }
-
-        /// <summary>
-        /// How the debuggees are built: "prebuilt" or "cli" (builds the debuggee during the test run with build and cli configuration).
-        /// </summary>
-        public string DebuggeeBuildProcess
-        {
-            get { return GetValue("DebuggeeBuildProcess")?.ToLowerInvariant(); }
-        }
-
-        /// <summary>
-        /// Debuggee sources and template project file will be retrieved from here: <DebuggeeSourceRoot>/<DebuggeeName>/[<DebuggeeName>]
-        /// </summary>
-        public string DebuggeeSourceRoot
-        {
-            get { return MakeCanonicalPath(GetValue("DebuggeeSourceRoot")); }
-        }
-
-        /// <summary>
-        /// Debuggee final sources/project file/binary outputs will be placed here: <DebuggeeBuildRoot>/<DebuggeeName>/
-        /// </summary>
-        public string DebuggeeBuildRoot
-        {
-            get { return MakeCanonicalPath(GetValue(DebuggeeBuildRootKey)); }
-        }
-
-        /// <summary>
-        /// Debuggee native binary dependencies will be retrieved from here.
-        /// </summary>
-        public string DebuggeeNativeLibRoot
-        {
-            get { return MakeCanonicalPath(GetValue("DebuggeeNativeLibRoot")); }
-        }
-
-        /// <summary>
-        /// The version of the Microsoft.NETCore.App package to reference when building the debuggee.
-        /// </summary>
-        public string BuildProjectMicrosoftNetCoreAppVersion
-        {
-            get { return GetValue("BuildProjectMicrosoftNetCoreAppVersion"); }
-        }
-
-        /// <summary>
-        /// The framework type/version used to build the debuggee like "netcoreapp2.0" or "netstandard1.0".
-        /// </summary>
-        public string BuildProjectFramework
-        {
-            get { return GetValue("BuildProjectFramework"); }
-        }
-
-        /// <summary>
-        /// Optional runtime identifier (RID) like "linux-x64" or "win-x86". If set, causes the debuggee to 
-        /// be built a as "standalone" dotnet cli project where the runtime is copied to the debuggee build 
-        /// root.
-        /// </summary>
-        public string BuildProjectRuntime
-        {
-            get { return GetValue("BuildProjectRuntime"); }
-        }
-
-        /// <summary>
-        /// The version of the Microsoft.NETCore.App package to reference when running the debuggee (i.e. 
-        /// using the dotnet cli --fx-version option).
-        /// </summary>
-        public string RuntimeFrameworkVersion 
-        {
-            get { return GetValue("RuntimeFrameworkVersion"); }
-        }
-
-        /// <summary>
-        /// The major portion of the runtime framework version
-        /// </summary>
-        public int RuntimeFrameworkVersionMajor
-        {
-            get {
-                string version = RuntimeFrameworkVersion;
-                if (version != null) {
-                    string[] parts = version.Split('.');
-                    if (parts.Length > 0) {
-                        if (int.TryParse(parts[0], out int major)) {
-                            return major;
-                        }
-                    }
-                }
-                throw new SkipTestException("RuntimeFrameworkVersion (major) is not valid");
-            }
-        }
-
-        /// <summary>
-        /// The type of PDB: "full" (Windows PDB) or "portable".
-        /// </summary>
-        public string DebugType
-        {
-            get { return GetValue(DebugTypeKey); }
-        }
-
-        /// <summary>
-        /// Either the local path to the dotnet cli to build or the URL of the runtime to download and install.
-        /// </summary>
-        public string CliPath
-        {
-            get { return MakeCanonicalPath(GetValue("CliPath")); }
-        }
-
-        /// <summary>
-        /// The local path to put the downloaded and decompressed runtime.
-        /// </summary>
-        public string CliCacheRoot
-        {
-            get { return MakeCanonicalPath(GetValue("CliCacheRoot")); }
-        }
-
-        /// <summary>
-        /// The version (i.e. 2.0.0) of the dotnet cli to use.
-        /// </summary>
-        public string CliVersion
-        {
-            get { return GetValue("CliVersion"); }
-        }
-
-        /// <summary>
-        /// The directory to cache the nuget packages on restore
-        /// </summary>
-        public string NuGetPackageCacheDir
-        {
-            get { return MakeCanonicalPath(GetValue("NuGetPackageCacheDir")); }
-        }
-
-        /// <summary>
-        /// The nuget package feeds separated by semicolons.
-        /// </summary>
-        public string NuGetPackageFeeds
-        {
-            get { return GetValue("NuGetPackageFeeds"); }
-        }
-
-        /// <summary>
-        /// If true, log the test output, etc. to the console.
-        /// </summary>
-        public bool LogToConsole
-        {
-            get { return bool.TryParse(GetValue("LogToConsole"), out bool b) && b; }
-        }
-
-        /// <summary>
-        /// The directory to put the test logs.
-        /// </summary>
-        public string LogDirPath
-        {
-            get { return MakeCanonicalPath(GetValue("LogDir")); }
-        }
-
-        /// <summary>
-        /// The "ILLink.Tasks" package version to reference or null.
-        /// </summary>
-        public string LinkerPackageVersion
-        {
-            get { return GetValue("LinkerPackageVersion"); }
-        }
-
-        #region Runtime Features properties
-
-        /// <summary>
-        /// Returns true if the "createdump" facility exists.
-        /// </summary>
-        public bool CreateDumpExists
-        {
-            get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor > 1; }
-        }
-
-        /// <summary>
-        /// Returns true if a stack overflow causes dump to be generated with createdump. 3.x has now started to
-        /// create dumps on stack overflow.
-        /// </summary>
-        public bool StackOverflowCreatesDump
-        {
-            get { return IsNETCore && RuntimeFrameworkVersionMajor >= 3; }
-        }
-
-        /// <summary>
-        /// Returns true if a stack overflow causes a SIGSEGV exception instead of aborting.
-        /// </summary>
-        public bool StackOverflowSIGSEGV
-        {
-            get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor == 1; }
-        }
-
-        #endregion
-
-        /// <summary>
-        /// Returns the configuration value for the key or null.
-        /// </summary>
-        /// <param name="key">name of the configuration value</param>
-        /// <returns>configuration value or null</returns>
-        public string GetValue(string key)
-        {
-            // unlike dictionary it is OK to ask for non-existent keys
-            // if the key doesn't exist the result is null
-            _settings.TryGetValue(key, out string settingValue);
-            return settingValue;
-        }
-
-        public static string MakeCanonicalExePath(string maybeRelativePath)
-        {
-            if (string.IsNullOrWhiteSpace(maybeRelativePath))
-            {
-                return null;
-            }
-            string maybeRelativePathWithExtension = maybeRelativePath;
-            if (OS.Kind == OSKind.Windows && !maybeRelativePath.EndsWith(".exe"))
-            {
-                maybeRelativePathWithExtension = maybeRelativePath + ".exe";
-            }
-            return MakeCanonicalPath(maybeRelativePathWithExtension);
-        }
-
-        public static string MakeCanonicalPath(string maybeRelativePath)
-        {
-            return MakeCanonicalPath(BaseDir, maybeRelativePath);
-        }
-
-        public static string MakeCanonicalPath(string baseDir, string maybeRelativePath)
-        {
-            if (string.IsNullOrWhiteSpace(maybeRelativePath))
-            {
-                return null;
-            }
-            // we will assume any path referencing an http endpoint is canonical already
-            if(maybeRelativePath.StartsWith("http:") ||
-               maybeRelativePath.StartsWith("https:"))
-            {
-                return maybeRelativePath;
-            }
-            string path = Path.IsPathRooted(maybeRelativePath) ? maybeRelativePath : Path.Combine(baseDir, maybeRelativePath);
-            path = Path.GetFullPath(path);
-            return OS.Kind != OSKind.Windows ? path.Replace('\\', '/') : path;
-        }
-
-        public override string ToString()
-        {
-            return TestProduct + "." + DebuggeeBuildProcess;
-        }
-    }
-
-    /// <summary>
-    /// The OS running
-    /// </summary>
-    public enum OSKind
-    {
-        Windows,
-        Linux,
-        OSX,
-        Unknown,
-    }
-
-    /// <summary>
-    /// The OS specific configuration
-    /// </summary>
-    public static class OS
-    {
-        static OS()
-        {
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
-            {
-                Kind = OSKind.Linux;
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-            {
-                Kind = OSKind.OSX;
-            }
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                Kind = OSKind.Windows;
-            }
-            else
-            {
-                // Default to Unknown
-                Kind = OSKind.Unknown;
-            }
-        }
-
-        /// <summary>
-        /// The OS the tests are running.
-        /// </summary>
-        public static OSKind Kind { get; private set; }
-
-        /// <summary>
-        /// The architecture the tests are running.  We are assuming that the test runner, the debugger and the debugger's target are all the same architecture.
-        /// </summary>
-        public static Architecture TargetArchitecture { get { return RuntimeInformation.ProcessArchitecture; } }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs b/src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs
deleted file mode 100644 (file)
index 177a7d7..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public class TestOutputProcessLogger : IProcessLogger
-    {
-        string _timeFormat = "mm\\:ss\\.fff";
-        ITestOutputHelper _output;
-        StringBuilder[] _lineBuffers;
-
-        public TestOutputProcessLogger(ITestOutputHelper output)
-        {
-            _output = output;
-            _lineBuffers = new StringBuilder[(int)ProcessStream.MaxStreams];
-        }
-
-        public void ProcessStarted(ProcessRunner runner)
-        {
-            lock (this)
-            {
-                _output.WriteLine("Running Process: " + runner.ReplayCommand);
-                _output.WriteLine("Working Directory: " + runner.WorkingDirectory);
-                IEnumerable<KeyValuePair<string,string>> additionalEnvVars = 
-                    runner.EnvironmentVariables.Where(kv => Environment.GetEnvironmentVariable(kv.Key) != kv.Value);
-
-                if(additionalEnvVars.Any())
-                {
-                    _output.WriteLine("Additional Environment Variables: " +
-                        string.Join(", ", additionalEnvVars.Select(kv => kv.Key + "=" + kv.Value)));
-                }
-                _output.WriteLine("{");
-            }
-        }
-
-        public virtual void Write(ProcessRunner runner, string data, ProcessStream stream)
-        {
-            lock (this)
-            {
-                AppendToLineBuffer(runner, stream, data);
-            }
-        }
-
-        public virtual void WriteLine(ProcessRunner runner, string data, ProcessStream stream)
-        {
-            lock (this)
-            {
-                StringBuilder lineBuffer = AppendToLineBuffer(runner, stream, data);
-                //Ensure all output is written even if it isn't a full line before we log input
-                if (stream == ProcessStream.StandardIn)
-                {
-                    FlushOutput();
-                }
-                _output.WriteLine(lineBuffer.ToString());
-                _lineBuffers[(int)stream] = null;
-            }
-        }
-
-        public virtual void ProcessExited(ProcessRunner runner)
-        {
-            lock (this)
-            {
-                TimeSpan offset = runner.StartTime - DateTime.Now;
-                _output.WriteLine("}");
-                _output.WriteLine("Exit code: " + runner.ExitCode + " ( " + offset.ToString(_timeFormat) + " elapsed)");
-                _output.WriteLine("");
-            }
-        }
-
-        public void ProcessKilled(ProcessRunner runner, KillReason reason)
-        {
-            lock (this)
-            {
-                TimeSpan offset = runner.StartTime - DateTime.Now;
-                string reasonText = "";
-                if (reason == KillReason.TimedOut)
-                {
-                    reasonText = "Process timed out";
-                }
-                else if (reason == KillReason.Unknown)
-                {
-                    reasonText = "Kill() was called";
-                }
-                _output.WriteLine("    Killing process: " + offset.ToString(_timeFormat) + ": " + reasonText);
-            }
-        }
-
-        protected void FlushOutput()
-        {
-            if (_lineBuffers[(int)ProcessStream.StandardOut] != null)
-            {
-                _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardOut].ToString());
-                _lineBuffers[(int)ProcessStream.StandardOut] = null;
-            }
-            if (_lineBuffers[(int)ProcessStream.StandardError] != null)
-            {
-                _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardError].ToString());
-                _lineBuffers[(int)ProcessStream.StandardError] = null;
-            }
-        }
-
-        private StringBuilder AppendToLineBuffer(ProcessRunner runner, ProcessStream stream, string data)
-        {
-            StringBuilder lineBuffer = _lineBuffers[(int)stream];
-            if (lineBuffer == null)
-            {
-                TimeSpan offset = runner.StartTime - DateTime.Now;
-                lineBuffer = new StringBuilder();
-                lineBuffer.Append("    ");
-                if (stream == ProcessStream.StandardError)
-                {
-                    lineBuffer.Append("STDERROR: ");
-                }
-                else if (stream == ProcessStream.StandardIn)
-                {
-                    lineBuffer.Append("STDIN: ");
-                }
-                lineBuffer.Append(offset.ToString(_timeFormat));
-                lineBuffer.Append(": ");
-                _lineBuffers[(int)stream] = lineBuffer;
-            }
-
-            // xunit has a bug where a non-printable character isn't properly escaped when
-            // it is written into the xml results which ultimately results in 
-            // the xml being improperly truncated. For example MDbg has a test case that prints
-            // \0 and dotnet tools print \u001B to colorize their console output.
-            foreach(char c in data)
-            {
-                if(!char.IsControl(c))
-                {
-                    lineBuffer.Append(c);
-                }
-            }
-            return lineBuffer;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs b/src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs
deleted file mode 100644 (file)
index 0bc1b6e..0000000
+++ /dev/null
@@ -1,257 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    public class TestRunner
-    {
-        /// <summary>
-        /// Run debuggee (without any debugger) and compare the console output to the regex specified.
-        /// </summary>
-        /// <param name="config">test config to use</param>
-        /// <param name="output">output helper</param>
-        /// <param name="testName">test case name</param>
-        /// <param name="debuggeeName">debuggee name (no path)</param>
-        /// <param name="outputRegex">regex to match on console (standard and error) output</param>
-        /// <returns></returns>
-        public static async Task<int> Run(TestConfiguration config, ITestOutputHelper output, string testName, string debuggeeName, string outputRegex)
-        {
-            OutputHelper outputHelper = null;
-            try
-            {
-                // Setup the logging from the options in the config file
-                outputHelper = ConfigureLogging(config, output, testName);
-
-                // Restore and build the debuggee. The debuggee name is lower cased because the 
-                // source directory name has been lowercased by the build system.
-                DebuggeeConfiguration debuggeeConfig = await DebuggeeCompiler.Execute(config, debuggeeName.ToLowerInvariant(), outputHelper);
-
-                outputHelper.WriteLine("Starting {0}", testName);
-                outputHelper.WriteLine("{");
-
-                // Get the full debuggee launch command line (includes the host if required)
-                string exePath = debuggeeConfig.BinaryExePath;
-                string arguments = debuggeeConfig.BinaryDirPath;
-                if (!string.IsNullOrWhiteSpace(config.HostExe))
-                {
-                    exePath = config.HostExe;
-                    arguments = Environment.ExpandEnvironmentVariables(string.Format("{0} {1} {2}", config.HostArgs, debuggeeConfig.BinaryExePath, debuggeeConfig.BinaryDirPath));
-                }
-
-                TestLogger testLogger = new TestLogger(outputHelper.IndentedOutput);
-                ProcessRunner processRunner = new ProcessRunner(exePath, arguments).
-                    WithLog(testLogger).
-                    WithTimeout(TimeSpan.FromMinutes(5));
-
-                processRunner.Start();
-
-                // Wait for the debuggee to finish before getting the debuggee output
-                int exitCode = await processRunner.WaitForExit();
-
-                string debuggeeStandardOutput = testLogger.GetStandardOutput();
-                string debuggeeStandardError = testLogger.GetStandardError();
-
-                // The debuggee output is all the stdout first and then all the stderr output last
-                string debuggeeOutput = debuggeeStandardOutput + debuggeeStandardError;
-                if (string.IsNullOrEmpty(debuggeeOutput))
-                {
-                    throw new Exception("No debuggee output");
-                }
-                // Remove any CR's in the match string because this assembly is built on Windows (with CRs) and
-                // ran on Linux/OS X (without CRs).
-                outputRegex = outputRegex.Replace("\r", "");
-
-                // Now match the debuggee output and regex match string
-                if (!new Regex(outputRegex, RegexOptions.Multiline).IsMatch(debuggeeOutput))
-                {
-                    throw new Exception(string.Format("\nDebuggee output:\n\n'{0}'\n\nDid not match the expression:\n\n'{1}'", debuggeeOutput, outputRegex));
-                }
-
-                return exitCode;
-            }
-            catch (Exception ex)
-            {
-                // Log the exception
-                outputHelper?.WriteLine(ex.ToString());
-                throw;
-            }
-            finally
-            {
-                outputHelper?.WriteLine("}");
-                outputHelper?.Dispose();
-            }
-        }
-
-        /// <summary>
-        /// Returns a test config for each PDB type supported by the product/platform.
-        /// </summary>
-        /// <param name="config">starting config</param>
-        /// <returns>new configs for each supported PDB type</returns>
-        public static IEnumerable<TestConfiguration> EnumeratePdbTypeConfigs(TestConfiguration config)
-        {
-            string[] pdbTypes = { "portable", "embedded" };
-
-            if (OS.Kind == OSKind.Windows)
-            {
-                if (config.IsNETCore)
-                {
-                    pdbTypes = new string[] { "portable", "full", "embedded" };
-                }
-                else
-                {
-                    // Don't change the config on the desktop/projectn projects
-                    pdbTypes = new string[] { "" };
-                }
-            }
-
-            foreach (string pdbType in pdbTypes)
-            {
-                if (string.IsNullOrWhiteSpace(pdbType))
-                {
-                    yield return config;
-                }
-                else
-                {
-                    yield return config.CloneWithNewDebugType(pdbType);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Returns an output helper for the specified config.
-        /// </summary>
-        /// <param name="config">test config</param>
-        /// <param name="output">starting output helper</param>
-        /// <param name="testName">test case name</param>
-        /// <returns>new output helper</returns>
-        public static TestRunner.OutputHelper ConfigureLogging(TestConfiguration config, ITestOutputHelper output, string testName)
-        {
-            FileTestOutputHelper fileLogger = null;
-            ConsoleTestOutputHelper consoleLogger = null;
-            if (!string.IsNullOrEmpty(config.LogDirPath))
-            {
-                string logFileName = testName + "." + config.ToString() + ".log";
-                string logPath = Path.Combine(config.LogDirPath, logFileName);
-                fileLogger = new FileTestOutputHelper(logPath, FileMode.Append);
-            }
-            if (config.LogToConsole)
-            {
-                consoleLogger = new ConsoleTestOutputHelper();
-            }
-            return new TestRunner.OutputHelper(output, fileLogger, consoleLogger);
-        }
-
-        public class OutputHelper : ITestOutputHelper, IDisposable
-        {
-            readonly ITestOutputHelper _output;
-            readonly FileTestOutputHelper _fileLogger;
-            readonly ConsoleTestOutputHelper _consoleLogger;
-
-            public readonly ITestOutputHelper IndentedOutput;
-
-            public OutputHelper(ITestOutputHelper output, FileTestOutputHelper fileLogger, ConsoleTestOutputHelper consoleLogger)
-            {
-                _output = output;
-                _fileLogger = fileLogger;
-                _consoleLogger = consoleLogger;
-                IndentedOutput = new IndentedTestOutputHelper(this);
-            }
-
-            public void WriteLine(string message)
-            {
-                _output.WriteLine(message);
-                _fileLogger?.WriteLine(message);
-                _consoleLogger?.WriteLine(message);
-            }
-
-            public void WriteLine(string format, params object[] args)
-            {
-                _output.WriteLine(format, args);
-                _fileLogger?.WriteLine(format, args);
-                _consoleLogger?.WriteLine(format, args);
-            }
-
-            public void Dispose()
-            {
-                _fileLogger?.Dispose();
-            }
-        }
-
-        public class TestLogger : TestOutputProcessLogger
-        {
-            readonly StringBuilder _standardOutput;
-            readonly StringBuilder _standardError;
-
-            public TestLogger(ITestOutputHelper output)
-                : base(output)
-            {
-                lock (this)
-                {
-                    _standardOutput = new StringBuilder();
-                    _standardError = new StringBuilder();
-                }
-            }
-
-            public string GetStandardOutput()
-            {
-                lock (this)
-                {
-                    return _standardOutput.ToString();
-                }
-            }
-
-            public string GetStandardError()
-            {
-                lock (this)
-                {
-                    return _standardError.ToString();
-                }
-            }
-
-            public override void Write(ProcessRunner runner, string data, ProcessStream stream)
-            {
-                lock (this)
-                {
-                    base.Write(runner, data, stream);
-                    switch (stream)
-                    {
-                        case ProcessStream.StandardOut:
-                            _standardOutput.Append(data);
-                            break;
-
-                        case ProcessStream.StandardError:
-                            _standardError.Append(data);
-                            break;
-                    }
-                }
-            }
-
-            public override void WriteLine(ProcessRunner runner, string data, ProcessStream stream)
-            {
-                lock (this)
-                {
-                    base.WriteLine(runner, data, stream);
-                    switch (stream)
-                    {
-                        case ProcessStream.StandardOut:
-                            _standardOutput.AppendLine(data);
-                            break;
-
-                        case ProcessStream.StandardError:
-                            _standardError.AppendLine(data);
-                            break;
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/TestStep.cs
deleted file mode 100644 (file)
index 1931fdd..0000000
+++ /dev/null
@@ -1,636 +0,0 @@
-// 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 System;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using System.Xml.Linq;
-using Xunit.Abstractions;
-
-namespace Microsoft.Diagnostic.TestHelpers
-{
-    /// <summary>
-    /// An incremental atomic unit of work in the process of running a test. A test
-    /// can consist of multiple processes running across different machines at
-    /// different times. The TestStep supports:
-    /// 1) coordination between test processes to ensure each step runs only once
-    /// 2) disk based persistence so that later steps in different processes can
-    ///    reload the state of earlier steps
-    /// 3) Pretty printing logs
-    /// 4) TODO: Dependency analysis to determine if the cached output of a previous step
-    ///    execution is still valid
-    /// </summary>
-    public class TestStep
-    {
-        string _logFilePath;
-        string _stateFilePath;
-        TimeSpan _timeout;
-
-        public TestStep(string logFilePath, string friendlyName)
-        {
-            _logFilePath = logFilePath;
-            _stateFilePath = Path.ChangeExtension(_logFilePath, "state.txt");
-            _timeout = TimeSpan.FromMinutes(20);
-            FriendlyName = friendlyName;
-        }
-
-        public string FriendlyName { get; private set; }
-
-        async public Task Execute(ITestOutputHelper output)
-        {
-            // if this step is in progress on another thread, wait for it
-            TestStepState stepState = await AcquireStepStateLock(output);
-
-            //if this thread wins the race we do the work on this thread, otherwise
-            //we log the winner's saved output
-            if (stepState.RunState != TestStepRunState.InProgress)
-            {
-                LogHeader(stepState, true, output);
-                LogPreviousResults(stepState, output);
-                LogFooter(stepState, output);
-                ThrowExceptionIfFaulted(stepState);
-            }
-            else
-            {
-                await UncachedExecute(stepState, output);
-            }
-        }
-
-        protected virtual Task DoWork(ITestOutputHelper output)
-        {
-            output.WriteLine("Overload the default DoWork implementation in order to run useful work");
-            return Task.Delay(0);
-        }
-
-        private async Task UncachedExecute(TestStepState stepState, ITestOutputHelper output)
-        {
-            using (FileTestOutputHelper stepLog = new FileTestOutputHelper(_logFilePath))
-            {
-                try
-                {
-                    LogHeader(stepState, false, output);
-                    MultiplexTestOutputHelper mux = new MultiplexTestOutputHelper(new IndentedTestOutputHelper(output), stepLog);
-                    await DoWork(mux);
-                    stepState = stepState.Complete();
-                }
-                catch (Exception e)
-                {
-                    stepState = stepState.Fault(e.Message, e.StackTrace);
-                }
-                finally
-                {
-                    LogFooter(stepState, output);
-                    await WriteFinalStepState(stepState, output);
-                    ThrowExceptionIfFaulted(stepState);
-                }
-            }
-        }
-
-        private bool TryWriteInitialStepState(TestStepState state, ITestOutputHelper output)
-        {
-            // To ensure the file is atomically updated we write the contents to a temporary
-            // file, then move it to the final location
-            try
-            {
-                string tempPath = Path.GetTempFileName();
-                try
-                {
-                    File.WriteAllText(tempPath, state.SerializeInitialState());
-                    Directory.CreateDirectory(Path.GetDirectoryName(_stateFilePath));
-                    File.Move(tempPath, _stateFilePath);
-                    return true;
-                }
-                finally
-                {
-                    File.Delete(tempPath);
-                }
-                
-            }
-            catch (IOException ex)
-            {
-                output.WriteLine("Exception writing state file {0} {1}", _stateFilePath, ex.ToString());
-                return false;
-            }
-        }
-
-        private bool TryOpenExistingStepStateFile(out TestStepState stepState, ITestOutputHelper output)
-        {
-            stepState = null;
-            try
-            {
-                if (!Directory.Exists(Path.GetDirectoryName(_stateFilePath)))
-                {
-                    return false;
-                }
-                bool result = TestStepState.TryParse(File.ReadAllText(_stateFilePath), out stepState);
-                if (!result)
-                {
-                    output.WriteLine("TryParse failed on opening existing state file {0}", _stateFilePath);
-                }
-                return result;
-            }
-            catch (IOException ex)
-            {
-                output.WriteLine("Exception opening existing state file {0} {1}", _stateFilePath, ex.ToString());
-                return false;
-            }
-        }
-
-        async private Task WriteFinalStepState(TestStepState stepState, ITestOutputHelper output)
-        {
-            const int NumberOfRetries = 5;
-            FileStream stepStateStream = null;
-
-            // Retry few times because the state file may be open temporarily by another thread or process.
-            for (int retries = 0; retries < NumberOfRetries; retries++)
-            {
-                try
-                {
-                    stepStateStream = File.Open(_stateFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
-                    break;
-                }
-                catch (IOException ex)
-                {
-                    output.WriteLine("WriteFinalStepState exception {0} retry #{1}", ex.ToString(), retries);
-                    if (retries >= (NumberOfRetries - 1))
-                    {
-                        throw;
-                    }
-                }
-            }
-
-            using (stepStateStream)
-            {
-                stepStateStream.Seek(0, SeekOrigin.End);
-                StreamWriter writer = new StreamWriter(stepStateStream);
-                await writer.WriteAsync(Environment.NewLine + stepState.SerializeFinalState());
-                await writer.FlushAsync();
-            }
-        }
-
-        private void LogHeader(TestStepState stepState, bool cached, ITestOutputHelper output)
-        {
-            string cachedText = cached ? " (CACHED)" : "";
-            output.WriteLine("[" + stepState.StartTime + "] " + FriendlyName + cachedText);
-            output.WriteLine("Process: " + stepState.ProcessName + "(ID: 0x" + stepState.ProcessID.ToString("x") + ") on " + stepState.Machine);
-            output.WriteLine("{");
-        }
-
-        private void LogFooter(TestStepState stepState, ITestOutputHelper output)
-        {
-            output.WriteLine("}");
-            string elapsedTime = null;
-            if (stepState.RunState == TestStepRunState.InProgress)
-            {
-                output.WriteLine(FriendlyName + " Not Complete");
-                output.WriteLine(stepState.ErrorMessage);
-            }
-            else
-            {
-                elapsedTime = (stepState.CompleteTime.Value - stepState.StartTime).ToString("mm\\:ss\\.fff");
-            }
-            if (stepState.RunState == TestStepRunState.Complete)
-            {
-                output.WriteLine(FriendlyName + " Complete (" + elapsedTime + " elapsed)");
-            }
-            else if (stepState.RunState == TestStepRunState.Faulted)
-            {
-                output.WriteLine(FriendlyName + " Faulted (" + elapsedTime + " elapsed)");
-                output.WriteLine(stepState.ErrorMessage);
-                output.WriteLine(stepState.ErrorStackTrace);
-            }
-            output.WriteLine("");
-            output.WriteLine("");
-        }
-
-        private async Task<TestStepState> AcquireStepStateLock(ITestOutputHelper output)
-        {
-            TestStepState initialStepState = new TestStepState();
-            
-            bool stepStateFileExists = false;
-            while (true)
-            {
-                TestStepState openedStepState = null;
-                stepStateFileExists = File.Exists(_stateFilePath);
-                if (!stepStateFileExists && TryWriteInitialStepState(initialStepState, output))
-                {
-                    // this thread gets to do the work, persist the initial lock state
-                    return initialStepState;
-                }
-
-                if (stepStateFileExists && TryOpenExistingStepStateFile(out openedStepState, output))
-                {
-                    if (!ShouldReuseCachedStepState(openedStepState))
-                    {
-                        try
-                        {
-                            File.Delete(_stateFilePath);
-                            continue;
-                        }
-                        catch (IOException ex)
-                        {
-                            output.WriteLine("Exception deleting state file {0} {1}", _stateFilePath, ex.ToString());
-                        }
-                    }
-                    else if (openedStepState.RunState != TestStepRunState.InProgress)
-                    {
-                        // we can reuse the work and it is finished - stop waiting and return it
-                        return openedStepState;
-                    }
-                }
-
-                // If we get here we are either:
-                // a) Waiting for some other thread (potentially in another process) to complete the work
-                // b) Waiting for a hopefully transient IO issue to resolve so that we can determine whether or not the work has already been claimed
-                //
-                // If we wait for too long in either case we will eventually timeout.
-                ThrowExceptionForIncompleteWorkIfNeeded(initialStepState, openedStepState, stepStateFileExists, output);
-                await Task.Delay(TimeSpan.FromSeconds(1));
-            }
-        }
-
-        private void ThrowExceptionForIncompleteWorkIfNeeded(TestStepState initialStepState, TestStepState openedStepState, bool stepStateFileExists, ITestOutputHelper output)
-        {
-            bool timeout = (DateTimeOffset.Now - initialStepState.StartTime > _timeout);
-            bool notFinishable = openedStepState != null &&
-                                 ShouldReuseCachedStepState(openedStepState) &&
-                                 openedStepState.RunState == TestStepRunState.InProgress &&
-                                 !IsOpenedStateChangeable(openedStepState);
-            if (timeout || notFinishable)
-            {
-                TestStepState currentState = openedStepState != null ? openedStepState : initialStepState;
-                LogHeader(currentState, true, output);
-                StringBuilder errorMessage = new StringBuilder();
-                if (timeout)
-                {
-                    errorMessage.Append("Timeout after " + _timeout + ". ");
-                }
-                if (!stepStateFileExists)
-                {
-                    errorMessage.Append("Unable to create file:" + Environment.NewLine +
-                        _stateFilePath);
-                }
-                else if (openedStepState == null)
-                {
-                    errorMessage.AppendLine("Unable to parse file:" + Environment.NewLine +
-                        _stateFilePath);
-                }
-                else
-                {
-                    // these error cases should have a valid previous log we can restore
-                    Debug.Assert(currentState == openedStepState);
-                    LogPreviousResults(currentState, output);
-
-                    errorMessage.AppendLine("This step was not marked complete in: " + Environment.NewLine +
-                                            _stateFilePath);
-
-                    if (!IsPreviousMachineSame(openedStepState))
-                    {
-                        errorMessage.AppendLine("The current machine (" + Environment.MachineName + ") differs from the one which ran the step originally (" + currentState.Machine + ")." + Environment.NewLine +
-                                                "Perhaps the original process (ID: 0x" + currentState.ProcessID.ToString("x") + ") executing the work exited unexpectedly or the file was" + Environment.NewLine +
-                                                "copied to this machine before the work was complete?");
-                    }
-                    else if (IsPreviousMachineSame(openedStepState) && !IsPreviousProcessRunning(openedStepState))
-                    {
-                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " the process executing this step (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
-                                                "is no longer running. Perhaps it was killed or exited unexpectedly?");
-                    }
-                    else if (openedStepState.ProcessID != Process.GetCurrentProcess().Id)
-                    {
-                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " the process executing this step (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
-                                                "is still running. The process may be hung or running more slowly than expected?");
-                    }
-                    else
-                    {
-                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " this step should still be running on some other thread in this process (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
-                                                "Perhaps the work has deadlocked or is running more slowly than expected?");
-                    }
-
-                    string reuseMessage = GetReuseStepStateReason(openedStepState);
-                    if (reuseMessage == null)
-                    {
-                        reuseMessage = "Deleting the file to retry the test step was attempted automatically, but failed.";
-                    }
-                    else
-                    {
-                        reuseMessage = "Deleting the file to retry the test step was not attempted automatically because " + reuseMessage + ".";
-                    }
-                    errorMessage.Append(reuseMessage);
-                }
-                currentState = currentState.Incomplete(errorMessage.ToString());
-                LogFooter(currentState, output);
-                if (timeout)
-                {
-                    throw new TestStepException("Timeout waiting for " + FriendlyName + " step to complete." + Environment.NewLine + errorMessage.ToString());
-                }
-                else
-                {
-                    throw new TestStepException(FriendlyName + " step can not be completed." + Environment.NewLine + errorMessage.ToString());
-                }
-            }
-        }
-
-        private static bool ShouldReuseCachedStepState(TestStepState openedStepState)
-        {
-            return (GetReuseStepStateReason(openedStepState) != null);
-        }
-
-        private static string GetReuseStepStateReason(TestStepState openedStepState)
-        {
-            //This heuristic may need to change, in some cases it is probably too eager to
-            //reuse past results when we wanted to retest something. 
-
-            if (openedStepState.RunState == TestStepRunState.Complete)
-            {
-                return "succesful steps are always reused";
-            }
-            else if(!IsPreviousMachineSame(openedStepState))
-            {
-                return "steps on run on other machines are always reused, regardless of success";
-            }
-            else if(IsPreviousProcessRunning(openedStepState))
-            {
-                return "steps run in currently executing processes are always reused, regardless of success";
-            }
-            else
-            {
-                return null;
-            }
-        }
-
-        private static bool IsPreviousMachineSame(TestStepState openedStepState)
-        {
-            return Environment.MachineName == openedStepState.Machine;
-        }
-
-        private static bool IsPreviousProcessRunning(TestStepState openedStepState)
-        {
-            Debug.Assert(IsPreviousMachineSame(openedStepState));
-            return (Process.GetProcesses().Any(p => p.Id == openedStepState.ProcessID && p.ProcessName == openedStepState.ProcessName));
-        }
-
-        private static bool IsOpenedStateChangeable(TestStepState openedStepState)
-        {
-            return (openedStepState.RunState == TestStepRunState.InProgress && 
-                    IsPreviousMachineSame(openedStepState) &&
-                    IsPreviousProcessRunning(openedStepState));
-        }
-
-        private void LogPreviousResults(TestStepState cachedTaskState, ITestOutputHelper output)
-        {
-            ITestOutputHelper indentedOutput = new IndentedTestOutputHelper(output);
-            try
-            {
-                string[] lines = File.ReadAllLines(_logFilePath);
-                foreach (string line in lines)
-                {
-                    indentedOutput.WriteLine(line);
-                }
-            }
-            catch (IOException e)
-            {
-                string errorMessage = "Error accessing task log file: " + _logFilePath + Environment.NewLine +
-                                      e.GetType().FullName + ": " + e.Message;
-                indentedOutput.WriteLine(errorMessage);
-            }
-        }
-
-        private void ThrowExceptionIfFaulted(TestStepState cachedStepState)
-        {
-            if(cachedStepState.RunState == TestStepRunState.Faulted)
-            {
-                throw new TestStepException(FriendlyName, cachedStepState.ErrorMessage, cachedStepState.ErrorStackTrace);
-            }
-        }
-
-        enum TestStepRunState
-        {
-            InProgress,
-            Complete,
-            Faulted
-        }
-
-        class TestStepState
-        {
-            public TestStepState()
-            {
-                RunState = TestStepRunState.InProgress;
-                Machine = Environment.MachineName;
-                ProcessID = Process.GetCurrentProcess().Id;
-                ProcessName = Process.GetCurrentProcess().ProcessName;
-                StartTime = DateTimeOffset.Now;
-            }
-            public TestStepState(TestStepRunState runState,
-                                 string machine,
-                                 int pid,
-                                 string processName,
-                                 DateTimeOffset startTime,
-                                 DateTimeOffset? completeTime,
-                                 string errorMessage,
-                                 string errorStackTrace)
-            {
-                RunState = runState;
-                Machine = machine;
-                ProcessID = pid;
-                ProcessName = processName;
-                StartTime = startTime;
-                CompleteTime = completeTime;
-                ErrorMessage = errorMessage;
-                ErrorStackTrace = errorStackTrace;
-            }
-            public TestStepRunState RunState { get; private set; }
-            public string Machine { get; private set; }
-            public int ProcessID { get; private set; }
-            public string ProcessName { get; private set; }
-            public string ErrorMessage { get; private set; }
-            public string ErrorStackTrace { get; private set; }
-            public DateTimeOffset StartTime { get; private set; }
-            public DateTimeOffset? CompleteTime { get; private set; }
-
-            public TestStepState Incomplete(string errorMessage)
-            {
-                return WithFinalState(TestStepRunState.InProgress, null, errorMessage, null);
-            }
-
-            public TestStepState Fault(string errorMessage, string errorStackTrace)
-            {
-                return WithFinalState(TestStepRunState.Faulted, DateTimeOffset.Now, errorMessage, errorStackTrace);
-            }
-
-            public TestStepState Complete()
-            {
-                return WithFinalState(TestStepRunState.Complete, DateTimeOffset.Now, null, null);
-            }
-
-            TestStepState WithFinalState(TestStepRunState runState, DateTimeOffset? taskCompleteTime, string errorMessage, string errorStackTrace)
-            {
-                return new TestStepState(runState, Machine, ProcessID, ProcessName, StartTime, taskCompleteTime, errorMessage, errorStackTrace);
-            }
-
-            public string SerializeInitialState()
-            {
-                XElement initState = new XElement("InitialStepState",
-                    new XElement("Machine", Machine),
-                    new XElement("ProcessID", "0x" + ProcessID.ToString("x")),
-                    new XElement("ProcessName", ProcessName),
-                    new XElement("StartTime", StartTime)
-                    );
-                return initState.ToString();
-            }
-
-            public string SerializeFinalState()
-            {
-                XElement finalState = new XElement("FinalStepState",
-                    new XElement("RunState", RunState)
-                    );
-                if (CompleteTime != null)
-                {
-                    finalState.Add(new XElement("CompleteTime", CompleteTime.Value));
-                }
-                if (ErrorMessage != null)
-                {
-                    finalState.Add(new XElement("ErrorMessage", ErrorMessage));
-                }
-                if (ErrorStackTrace != null)
-                {
-                    finalState.Add(new XElement("ErrorStackTrace", ErrorStackTrace));
-                }
-                return finalState.ToString();
-            }
-
-            public static bool TryParse(string text, out TestStepState parsedState)
-            {
-                parsedState = null;
-                try
-                {
-                    // The XmlReader is not happy with two root nodes so we crudely split them.
-                    int indexOfInitialStepStateElementEnd = text.IndexOf("</InitialStepState>");
-                    if(indexOfInitialStepStateElementEnd == -1)
-                    {
-                        return false;
-                    }
-                    int splitIndex = indexOfInitialStepStateElementEnd + "</InitialStepState>".Length;
-                    string initialStepStateText = text.Substring(0, splitIndex);
-                    string finalStepStateText = text.Substring(splitIndex);
-
-                    XElement initialStepStateElement = XElement.Parse(initialStepStateText);
-                    if (initialStepStateElement == null || initialStepStateElement.Name != "InitialStepState")
-                    {
-                        return false;
-                    }
-                    XElement machineElement = initialStepStateElement.Element("Machine");
-                    if (machineElement == null || string.IsNullOrWhiteSpace(machineElement.Value))
-                    {
-                        return false;
-                    }
-                    string machine = machineElement.Value;
-                    XElement processIDElement = initialStepStateElement.Element("ProcessID");
-                    int processID;
-                    if (processIDElement == null ||
-                        !processIDElement.Value.StartsWith("0x"))
-                    {
-                        return false;
-                    }
-                    string processIdNumberText = processIDElement.Value.Substring("0x".Length);
-                    if (!int.TryParse(processIdNumberText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out processID))
-                    {
-                        return false;
-                    }
-                    string processName = null;
-                    XElement processNameElement = initialStepStateElement.Element("ProcessName");
-                    if (processNameElement != null)
-                    {
-                        processName = processNameElement.Value;
-                    }
-                    DateTimeOffset startTime;
-                    XElement startTimeElement = initialStepStateElement.Element("StartTime");
-                    if (startTimeElement == null || !DateTimeOffset.TryParse(startTimeElement.Value, out startTime))
-                    {
-                        return false;
-                    }
-                    parsedState = new TestStepState(TestStepRunState.InProgress, machine, processID, processName, startTime, null, null, null);
-                    TryParseFinalState(finalStepStateText, ref parsedState);
-                    return true;
-                }
-                catch (XmlException)
-                {
-                    return false;
-                }
-            }
-
-            private static void TryParseFinalState(string text, ref TestStepState taskState)
-            {
-                // If there are errors reading the final state portion of the stream we need to treat it
-                // as if the stream had terminated at the end of the InitialTaskState node.
-                // This covers a small window of time when appending the FinalTaskState node is in progress.
-                //
-                if(string.IsNullOrWhiteSpace(text))
-                {
-                    return;
-                }
-                try
-                {
-                    XElement finalTaskStateElement = XElement.Parse(text);
-                    if (finalTaskStateElement == null || finalTaskStateElement.Name != "FinalStepState")
-                    {
-                        return;
-                    }
-                    XElement runStateElement = finalTaskStateElement.Element("RunState");
-                    TestStepRunState runState;
-                    if (runStateElement == null || !Enum.TryParse<TestStepRunState>(runStateElement.Value, out runState))
-                    {
-                        return;
-                    }
-                    DateTimeOffset? completeTime = null;
-                    XElement completeTimeElement = finalTaskStateElement.Element("CompleteTime");
-                    if (completeTimeElement != null)
-                    {
-                        DateTimeOffset tempCompleteTime;
-                        if (!DateTimeOffset.TryParse(completeTimeElement.Value, out tempCompleteTime))
-                        {
-                            return;
-                        }
-                        else
-                        {
-                            completeTime = tempCompleteTime;
-                        }
-                    }
-                    XElement errorMessageElement = finalTaskStateElement.Element("ErrorMessage");
-                    string errorMessage = null;
-                    if (errorMessageElement != null)
-                    {
-                        errorMessage = errorMessageElement.Value;
-                    }
-                    XElement errorStackTraceElement = finalTaskStateElement.Element("ErrorStackTrace");
-                    string errorStackTrace = null;
-                    if (errorStackTraceElement != null)
-                    {
-                        errorStackTrace = errorStackTraceElement.Value;
-                    }
-
-                    taskState = taskState.WithFinalState(runState, completeTime, errorMessage, errorStackTrace);
-                }
-                catch (XmlException) { }
-            }
-        }
-    }
-
-    public class TestStepException : Exception
-    {
-        public TestStepException(string errorMessage) :
-            base(errorMessage)
-        { }
-
-        public TestStepException(string stepName, string errorMessage, string stackTrace) :
-            base("The " + stepName + " test step failed." + Environment.NewLine +
-                 "Original Error: " + errorMessage + Environment.NewLine +
-                 stackTrace)
-        { }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs
deleted file mode 100644 (file)
index 21c5dbf..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace Xunit.Extensions
-{
-    public class SkipTestException : Exception
-    {
-        public SkipTestException(string reason)
-            : base(reason) { }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs
deleted file mode 100644 (file)
index 8421204..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-using Xunit;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostic.TestHelpers")]
-    public class SkippableFactAttribute : FactAttribute { }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs
deleted file mode 100644 (file)
index b249160..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer
-    {
-        readonly IMessageSink diagnosticMessageSink;
-
-        public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink)
-        {
-            this.diagnosticMessageSink = diagnosticMessageSink;
-        }
-
-        public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
-        {
-            yield return new SkippableFactTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(),  discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs
deleted file mode 100644 (file)
index 9a7fa30..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Linq;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    public class SkippableFactMessageBus : IMessageBus
-    {
-        readonly IMessageBus innerBus;
-
-        public SkippableFactMessageBus(IMessageBus innerBus)
-        {
-            this.innerBus = innerBus;
-        }
-
-        public int DynamicallySkippedTestCount { get; private set; }
-
-        public void Dispose() { }
-
-        public bool QueueMessage(IMessageSinkMessage message)
-        {
-            var testFailed = message as ITestFailed;
-            if (testFailed != null)
-            {
-                var exceptionType = testFailed.ExceptionTypes.FirstOrDefault();
-                if (exceptionType == typeof(SkipTestException).FullName)
-                {
-                    DynamicallySkippedTestCount++;
-                    return innerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault()));
-                }
-            }
-
-            // Nothing we care about, send it on its way
-            return innerBus.QueueMessage(message);
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs
deleted file mode 100644 (file)
index e031171..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    public class SkippableFactTestCase : XunitTestCase
-    {
-        [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
-        public SkippableFactTestCase() { }
-
-        public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null)
-            : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { }
-
-        public override async Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink,
-                                                        IMessageBus messageBus,
-                                                        object[] constructorArguments,
-                                                        ExceptionAggregator aggregator,
-                                                        CancellationTokenSource cancellationTokenSource)
-        {
-            var skipMessageBus = new SkippableFactMessageBus(messageBus);
-            var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource);
-            if (skipMessageBus.DynamicallySkippedTestCount > 0)
-            {
-                result.Failed -= skipMessageBus.DynamicallySkippedTestCount;
-                result.Skipped += skipMessageBus.DynamicallySkippedTestCount;
-            }
-
-            return result;
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs
deleted file mode 100644 (file)
index 5496ea4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostic.TestHelpers")]
-    public class SkippableTheoryAttribute : TheoryAttribute { }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs
deleted file mode 100644 (file)
index 40c4ab7..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    public class SkippableTheoryDiscoverer : IXunitTestCaseDiscoverer
-    {
-        readonly IMessageSink diagnosticMessageSink;
-        readonly TheoryDiscoverer theoryDiscoverer;
-
-        public SkippableTheoryDiscoverer(IMessageSink diagnosticMessageSink)
-        {
-            this.diagnosticMessageSink = diagnosticMessageSink;
-
-            theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink);
-        }
-
-        public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
-        {
-            var defaultMethodDisplay = discoveryOptions.MethodDisplayOrDefault();
-            var defaultMethodDisplayOptions = discoveryOptions.MethodDisplayOptionsOrDefault();
-
-            // Unlike fact discovery, the underlying algorithm for theories is complex, so we let the theory discoverer
-            // do its work, and do a little on-the-fly conversion into our own test cases.
-            return theoryDiscoverer.Discover(discoveryOptions, testMethod, factAttribute)
-                                   .Select(testCase => testCase is XunitTheoryTestCase
-                                                           ? (IXunitTestCase)new SkippableTheoryTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod)
-                                                           : new SkippableFactTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod, testCase.TestMethodArguments));
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs
deleted file mode 100644 (file)
index 46d2037..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
-    public class SkippableTheoryTestCase : XunitTheoryTestCase
-    {
-        [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
-        public SkippableTheoryTestCase() { }
-
-        public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod)
-            : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { }
-
-        public override async Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink,
-                                                        IMessageBus messageBus,
-                                                        object[] constructorArguments,
-                                                        ExceptionAggregator aggregator,
-                                                        CancellationTokenSource cancellationTokenSource)
-        {
-            // Duplicated code from SkippableFactTestCase. I'm sure we could find a way to de-dup with some thought.
-            var skipMessageBus = new SkippableFactMessageBus(messageBus);
-            var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource);
-            if (skipMessageBus.DynamicallySkippedTestCount > 0)
-            {
-                result.Failed -= skipMessageBus.DynamicallySkippedTestCount;
-                result.Skipped += skipMessageBus.DynamicallySkippedTestCount;
-            }
-
-            return result;
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt
deleted file mode 100644 (file)
index 39b2d65..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-Source in this directory is derived source at https://github.com/xunit/samples.xunit. The repo provided the following license:
-
-Copyright 2014 Outercurve Foundation
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs
new file mode 100644 (file)
index 0000000..e5b0882
--- /dev/null
@@ -0,0 +1,221 @@
+// 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 System;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// Acquires the CLI tools from a web endpoint, a local zip/tar.gz, or directly from a local path
+    /// </summary>
+    public class AcquireDotNetTestStep : TestStep
+    {
+        /// <summary>
+        /// Create a new AcquireDotNetTestStep
+        /// </summary>
+        /// <param name="remoteDotNetZipPath">
+        /// If non-null, the CLI tools will be downloaded from this web endpoint.
+        /// The path should use an http or https scheme and the remote file should be in .zip or .tar.gz format.
+        /// localDotNetZipPath must also be non-null to indicate where the downloaded archive will be cached</param>
+        /// <param name="localDotNetZipPath">
+        /// If non-null, the location of a .zip or .tar.gz compressed folder containing the CLI tools. This
+        /// must be a local file system or network file system path. 
+        /// localDotNetZipExpandDirPath must also be non-null to indicate where the expanded folder will be
+        /// stored.
+        /// localDotNetTarPath must be non-null if localDotNetZip points to a .tar.gz format archive, in order
+        /// to indicate where the .tar file will be cached</param>
+        /// <param name="localDotNetTarPath">
+        /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar
+        /// file. Otherwise this path is unused.</param>
+        /// <param name="localDotNetZipExpandDirPath">
+        /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the
+        /// archive. Otherwise this path is unused.</param>
+        /// <param name="localDotNetPath">
+        /// The path to the dotnet binary. When the CLI tools are being acquired from a compressed archive
+        /// this will presumably be a path inside the localDotNetZipExpandDirPath directory, otherwise
+        /// it can be any local file system path where the dotnet binary can be found.</param>
+        /// <param name="logFilePath">
+        /// The path where an activity log for this test step should be written.
+        /// </param>
+        /// 
+        public AcquireDotNetTestStep(
+            string remoteDotNetZipPath,
+            string localDotNetZipPath,
+            string localDotNetTarPath,
+            string localDotNetZipExpandDirPath,
+            string localDotNetPath,
+            string logFilePath)
+            : base(logFilePath, "Acquire DotNet Tools")
+        {
+            RemoteDotNetPath = remoteDotNetZipPath;
+            LocalDotNetZipPath = localDotNetZipPath;
+            if (localDotNetZipPath != null && localDotNetZipPath.EndsWith(".tar.gz"))
+            {
+                LocalDotNetTarPath = localDotNetTarPath;
+            }
+            if (localDotNetZipPath != null)
+            {
+                LocalDotNetZipExpandDirPath = localDotNetZipExpandDirPath;
+            }
+            LocalDotNetPath = localDotNetPath;
+        }
+
+        /// <summary>
+        /// If non-null, the CLI tools will be downloaded from this web endpoint.
+        /// The path should use an http or https scheme and the remote file should be in .zip or .tar.gz format.
+        /// </summary>
+        public string RemoteDotNetPath { get; private set; }
+
+        /// <summary>
+        /// If non-null, the location of a .zip or .tar.gz compressed folder containing the CLI tools. This
+        /// is a local file system or network file system path. 
+        /// </summary>
+        public string LocalDotNetZipPath { get; private set; }
+
+        /// <summary>
+        /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar
+        /// file. Otherwise null.
+        /// </summary>
+        public string LocalDotNetTarPath { get; private set; }
+
+        /// <summary>
+        /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the
+        /// archive. Otherwise null.
+        /// </summary>
+        public string LocalDotNetZipExpandDirPath { get; private set; }
+
+        /// <summary>
+        /// The path to the dotnet binary when the test step is complete.
+        /// </summary>
+        public string LocalDotNetPath { get; private set; }
+
+        /// <summary>
+        /// Returns true, if there any actual work to do (like downloading, unziping or untaring).
+        /// </summary>
+        public bool AnyWorkToDo { get { return RemoteDotNetPath != null || LocalDotNetZipPath != null; } }
+
+        async protected override Task DoWork(ITestOutputHelper output)
+        {
+            if (RemoteDotNetPath != null)
+            {
+                await DownloadFile(RemoteDotNetPath, LocalDotNetZipPath, output);
+            }
+            if (LocalDotNetZipPath != null)
+            {
+                if (LocalDotNetZipPath.EndsWith(".zip"))
+                {
+                    await Unzip(LocalDotNetZipPath, LocalDotNetZipExpandDirPath, output);
+                }
+                else if(LocalDotNetZipPath.EndsWith(".tar.gz"))
+                {
+                    await UnGZip(LocalDotNetZipPath, LocalDotNetTarPath, output);
+                    await Untar(LocalDotNetTarPath, LocalDotNetZipExpandDirPath, output);
+                }
+                else
+                {
+                    output.WriteLine("Unsupported compression format: " + LocalDotNetZipPath);
+                    throw new NotSupportedException("Unsupported compression format: " + LocalDotNetZipPath);
+                }
+            }
+            output.WriteLine("Dotnet path: " + LocalDotNetPath);
+            if (!File.Exists(LocalDotNetPath))
+            {
+                throw new FileNotFoundException(LocalDotNetPath + " not found");
+            }
+        }
+
+        async static Task DownloadFile(string remotePath, string localPath, ITestOutputHelper output)
+        {
+            output.WriteLine("Downloading: " + remotePath + " -> " + localPath);
+            Directory.CreateDirectory(Path.GetDirectoryName(localPath));
+            WebRequest request = HttpWebRequest.Create(remotePath);
+            WebResponse response = await request.GetResponseAsync();
+            using (FileStream localZipStream = File.OpenWrite(localPath))
+            {
+                // TODO: restore the CopyToAsync code after System.Net.Http.dll is 
+                // updated to a newer version. The current old version has a bug 
+                // where the copy never finished.
+                // await response.GetResponseStream().CopyToAsync(localZipStream);
+                byte[] buffer = new byte[16 * 1024];
+                long bytesLeft = response.ContentLength;
+
+                while (bytesLeft > 0)
+                {
+                    int read = response.GetResponseStream().Read(buffer, 0, buffer.Length);
+                    if (read == 0)
+                        break;
+                    localZipStream.Write(buffer, 0, read);
+                    bytesLeft -= read;
+                }
+                output.WriteLine("Downloading finished");
+            }
+        }
+
+        async static Task UnGZip(string gzipPath, string expandedFilePath, ITestOutputHelper output)
+        {
+            output.WriteLine("Unziping: " + gzipPath + " -> " + expandedFilePath);
+            using (FileStream gzipStream = File.OpenRead(gzipPath))
+            {
+                using (GZipStream expandedStream = new GZipStream(gzipStream, CompressionMode.Decompress))
+                {
+                    using (FileStream targetFileStream = File.OpenWrite(expandedFilePath))
+                    {
+                        await expandedStream.CopyToAsync(targetFileStream);
+                    }
+                }
+            }
+        }
+
+        async static Task Unzip(string zipPath, string expandedDirPath, ITestOutputHelper output)
+        {
+            output.WriteLine("Unziping: " + zipPath + " -> " + expandedDirPath);
+            using (FileStream zipStream = File.OpenRead(zipPath))
+            {
+                ZipArchive zip = new ZipArchive(zipStream);
+                foreach (ZipArchiveEntry entry in zip.Entries)
+                {
+                    string extractedFilePath = Path.Combine(expandedDirPath, entry.FullName);
+                    Directory.CreateDirectory(Path.GetDirectoryName(extractedFilePath));
+                    using (Stream zipFileStream = entry.Open())
+                    {
+                        using (FileStream extractedFileStream = File.OpenWrite(extractedFilePath))
+                        {
+                            await zipFileStream.CopyToAsync(extractedFileStream);
+                        }
+                    }
+                }
+            }
+        }
+
+        async static Task Untar(string tarPath, string expandedDirPath, ITestOutputHelper output)
+        {
+            Directory.CreateDirectory(expandedDirPath);
+            string tarToolPath = null;
+            if (OS.Kind == OSKind.Linux)
+            {
+                tarToolPath = "/bin/tar";
+            }
+            else if (OS.Kind == OSKind.OSX)
+            {
+                tarToolPath = "/usr/bin/tar";
+            }
+            else
+            {
+                throw new NotSupportedException("Unknown where this OS stores the tar executable");
+            }
+
+            await new ProcessRunner(tarToolPath, "-xf " + tarPath).
+                   WithWorkingDirectory(expandedDirPath).
+                   WithLog(output).
+                   WithExpectedExitCode(0).
+                   Run();
+        }
+
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/AssertX.cs b/src/Microsoft.Diagnostics.TestHelpers/AssertX.cs
new file mode 100644 (file)
index 0000000..a28b80b
--- /dev/null
@@ -0,0 +1,81 @@
+// 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 System;
+using System.IO;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public static partial class AssertX
+    {
+        public static void DirectoryExists(string dirDescriptiveName, string dirPath, ITestOutputHelper output)
+        {
+            if (!Directory.Exists(dirPath))
+            {
+                string errorMessage = "Expected " + dirDescriptiveName + " to exist: " + dirPath;
+                output.WriteLine(errorMessage);
+                try
+                {
+                    string parentDir = dirPath;
+                    while (true)
+                    {
+                        if (Directory.Exists(Path.GetDirectoryName(parentDir)))
+                        {
+                            output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir));
+                            break;
+                        }
+                        if (Path.GetDirectoryName(parentDir) == parentDir)
+                        {
+                            output.WriteLine("Also unable to find any parent directory that exists");
+                            break;
+                        }
+                        parentDir = Path.GetDirectoryName(parentDir);
+                    }
+                }
+                catch (Exception e)
+                {
+                    output.WriteLine("Additional error while trying to diagnose missing directory:");
+                    output.WriteLine(e.GetType() + ": " + e.Message);
+                }
+                throw new DirectoryNotFoundException(errorMessage);
+            }
+        }
+
+        public static void FileExists(string fileDescriptiveName, string filePath, ITestOutputHelper output)
+        {
+            if (!File.Exists(filePath))
+            {
+                string errorMessage = "Expected " + fileDescriptiveName + " to exist: " + filePath;
+                output.WriteLine(errorMessage);
+                try
+                {
+                    string parentDir = filePath;
+                    while (true)
+                    {
+                        if (Directory.Exists(Path.GetDirectoryName(parentDir)))
+                        {
+                            output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir));
+                            break;
+                        }
+                        if (Path.GetDirectoryName(parentDir) == parentDir)
+                        {
+                            output.WriteLine("Also unable to find any parent directory that exists");
+                            break;
+                        }
+                        parentDir = Path.GetDirectoryName(parentDir);
+                    }
+                }
+                catch (Exception e)
+                {
+                    output.WriteLine("Additional error while trying to diagnose missing file:");
+                    output.WriteLine(e.GetType() + ": " + e.Message);
+                }
+                throw new FileNotFoundException(errorMessage);
+            }
+        }
+    }
+}
+
+
diff --git a/src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs
new file mode 100644 (file)
index 0000000..79d4ce3
--- /dev/null
@@ -0,0 +1,222 @@
+// 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 System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// This compiler acquires the CLI tools and uses them to build debuggees.
+    /// </summary>
+    /// <remarks>
+    /// The build process consists of the following steps:
+    ///   1. Acquire the CLI tools from the CliPath. This generally involves downloading them from the web and unpacking them.
+    ///   2. Create a source directory with the conventions that dotnet expects by copying it from the DebuggeeSourceRoot. Any project.template.json
+    ///      file that is found will be specialized by replacing macros with specific contents. This lets us decide the runtime and dependency versions
+    ///      at test execution time.
+    ///   3. Run dotnet restore in the newly created source directory
+    ///   4. Run dotnet build in the same directory
+    ///   5. Rename the built debuggee dll to an exe (so that it conforms to some historical conventions our tests expect)
+    ///   6. Copy any native dependencies from the DebuggeeNativeLibRoot to the debuggee output directory
+    /// </remarks>
+    public abstract class BaseDebuggeeCompiler : IDebuggeeCompiler
+    {
+        AcquireDotNetTestStep _acquireTask;
+        DotNetBuildDebuggeeTestStep _buildDebuggeeTask;
+
+        /// <summary>
+        /// Creates a new BaseDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build debuggees via dotnet build.
+        /// </summary>
+        /// <param name="config">
+        /// The test configuration that will be used to configure the build. The following configuration options should be set in the config:
+        ///   CliPath                                  The location to get the CLI tools from, either as a .zip/.tar.gz at a web endpoint, a .zip/.tar.gz
+        ///                                            at a local filesystem path, or the dotnet binary at a local filesystem path
+        ///   WorkingDir                               Temporary storage for CLI tools compressed archives downloaded from the internet will be stored here
+        ///   CliCacheRoot                             The final CLI tools will be expanded and cached here
+        ///   DebuggeeSourceRoot                       Debuggee sources and template project file will be retrieved from here
+        ///   DebuggeeNativeLibRoot                    Debuggee native binary dependencies will be retrieved from here
+        ///   DebuggeeBuildRoot                        Debuggee final sources/project file/binary outputs will be placed here
+        ///   BuildProjectRuntime                      The runtime moniker to be built
+        ///   BuildProjectMicrosoftNETCoreAppVersion   The nuget package version of Microsoft.NETCore.App package to build against for debuggees that references this library
+        ///   NugetPackageCacheDir                     The directory where NuGet packages are cached during restore
+        ///   NugetFeeds                               The set of nuget feeds that are used to search for packages during restore
+        /// </param>
+        /// <param name="debuggeeName">
+        ///   The name of the debuggee to be built, from which various build file paths are constructed. Before build it is assumed that:
+        ///     Debuggee sources are located at               config.DebuggeeSourceRoot/debuggeeName/
+        ///     Debuggee native dependencies are located at   config.DebuggeeNativeLibRoot/debuggeeName/
+        ///
+        ///   After the build:
+        ///     Debuggee build outputs will be created at     config.DebuggeeNativeLibRoot/debuggeeName/
+        ///     A log of the build is stored at               config.DebuggeeNativeLibRoot/debuggeeName.txt
+        /// </param>
+        public BaseDebuggeeCompiler(TestConfiguration config, string debuggeeName)
+        {
+            _acquireTask = ConfigureAcquireDotNetTask(config);
+            _buildDebuggeeTask = ConfigureDotNetBuildDebuggeeTask(config, _acquireTask.LocalDotNetPath, config.CliVersion, debuggeeName);
+        }
+
+        async public Task<DebuggeeConfiguration> Execute(ITestOutputHelper output)
+        {
+            if (_acquireTask.AnyWorkToDo)
+            {
+                await _acquireTask.Execute(output);
+            }
+            await _buildDebuggeeTask.Execute(output);
+            return new DebuggeeConfiguration(_buildDebuggeeTask.DebuggeeProjectDirPath,
+                                             _buildDebuggeeTask.DebuggeeBinaryDirPath,
+                                             _buildDebuggeeTask.DebuggeeBinaryExePath ?? _buildDebuggeeTask.DebuggeeBinaryDllPath);
+        }
+
+        public static AcquireDotNetTestStep ConfigureAcquireDotNetTask(TestConfiguration config)
+        {
+            string remoteCliZipPath = null;
+            string localCliZipPath = null;
+            string localCliTarPath = null;
+            string localCliExpandedDirPath = null;
+
+            string dotNetPath = config.CliPath;
+            if (dotNetPath.StartsWith("http:") || dotNetPath.StartsWith("https:"))
+            {
+                remoteCliZipPath = dotNetPath;
+                dotNetPath = Path.Combine(config.WorkingDir, "dotnet_zip", Path.GetFileName(remoteCliZipPath));
+            }
+            if (dotNetPath.EndsWith(".zip") || dotNetPath.EndsWith(".tar.gz"))
+            {
+                localCliZipPath = dotNetPath;
+                string cliVersionDirName = null;
+                if (dotNetPath.EndsWith(".tar.gz"))
+                {
+                    localCliTarPath = localCliZipPath.Substring(0, dotNetPath.Length - 3);
+                    cliVersionDirName = Path.GetFileNameWithoutExtension(localCliTarPath);
+                }
+                else
+                {
+                    cliVersionDirName = Path.GetFileNameWithoutExtension(localCliZipPath);
+                }
+
+                localCliExpandedDirPath = Path.Combine(config.CliCacheRoot, cliVersionDirName);
+                dotNetPath = Path.Combine(localCliExpandedDirPath, OS.Kind == OSKind.Windows ? "dotnet.exe" : "dotnet");
+            }
+            string acquireLogDir = Path.GetDirectoryName(Path.GetDirectoryName(dotNetPath));
+            string acquireLogPath = Path.Combine(acquireLogDir, Path.GetDirectoryName(dotNetPath) + ".acquisition_log.txt");
+            return new AcquireDotNetTestStep(
+                remoteCliZipPath,
+                localCliZipPath,
+                localCliTarPath,
+                localCliExpandedDirPath,
+                dotNetPath,
+                acquireLogPath);
+        }
+
+
+        protected static string GetInitialSourceDirPath(TestConfiguration config, string debuggeeName)
+        {
+            return Path.Combine(config.DebuggeeSourceRoot, debuggeeName);
+        }
+
+        protected static string GetDebuggeeNativeLibDirPath(TestConfiguration config, string debuggeeName)
+        {
+            return Path.Combine(config.DebuggeeNativeLibRoot, debuggeeName);
+        }
+
+        protected static string GetDebuggeeSolutionDirPath(string dotNetRootBuildDirPath, string debuggeeName)
+        {
+            return Path.Combine(dotNetRootBuildDirPath, debuggeeName);
+        }
+
+        protected static string GetDotNetRootBuildDirPath(TestConfiguration config)
+        {
+            return config.DebuggeeBuildRoot;
+        }
+
+        protected static string GetDebuggeeProjectDirPath(string debuggeeSolutionDirPath, string initialSourceDirPath, string debuggeeName)
+        {
+            string debuggeeProjectDirPath = debuggeeSolutionDirPath;
+            if (Directory.Exists(Path.Combine(initialSourceDirPath, debuggeeName)))
+            {
+                debuggeeProjectDirPath = Path.Combine(debuggeeSolutionDirPath, debuggeeName);
+            }
+            return debuggeeProjectDirPath;
+        }
+
+        protected virtual string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime)
+        {
+            string debuggeeBinaryDirPath = null;
+            if (runtime != null)
+            {
+                debuggeeBinaryDirPath = Path.Combine(debuggeeProjectDirPath, "bin", "Debug", framework, runtime);
+            }
+            else
+            {
+                debuggeeBinaryDirPath = Path.Combine(debuggeeProjectDirPath, "bin", "Debug", framework);
+            }
+            return debuggeeBinaryDirPath;
+        }
+
+        protected static string GetDebuggeeBinaryDllPath(string debuggeeBinaryDirPath, string debuggeeName)
+        {
+            return Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".dll");
+        }
+
+        protected static string GetDebuggeeBinaryExePath(string debuggeeBinaryDirPath, string debuggeeName)
+        {
+            return Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".exe");
+        }
+
+        protected static string GetLogPath(TestConfiguration config, string framework, string runtime, string debuggeeName)
+        {
+            string version = config.BuildProjectMicrosoftNetCoreAppVersion;
+            return Path.Combine(GetDotNetRootBuildDirPath(config), $"{framework}-{runtime ?? "any"}-{debuggeeName}.txt");
+        }
+
+        protected static Dictionary<string, string> GetNugetFeeds(TestConfiguration config)
+        {
+            Dictionary<string, string> nugetFeeds = new Dictionary<string, string>();
+            if(!string.IsNullOrWhiteSpace(config.NuGetPackageFeeds))
+            {
+                string[] feeds = config.NuGetPackageFeeds.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+                foreach(string feed in feeds)
+                {
+                    string[] feedParts = feed.Trim().Split('=');
+                    if(feedParts.Length != 2)
+                    {
+                        throw new Exception("Expected feed \'" + feed + "\' to value <key>=<value> format");
+                    }
+                    nugetFeeds.Add(feedParts[0], feedParts[1]);
+                }
+            }
+            return nugetFeeds;
+        }
+
+        protected static string GetRuntime(TestConfiguration config)
+        {
+            return config.BuildProjectRuntime;
+        }
+
+        protected abstract string GetFramework(TestConfiguration config);
+
+        //we anticipate source paths like this:
+        //InitialSource:        <DebuggeeSourceRoot>/<DebuggeeName>
+        //DebuggeeNativeLibDir: <DebuggeeNativeLibRoot>/<DebuggeeName>
+        //DotNetRootBuildDir:   <DebuggeeBuildRoot>
+        //DebuggeeSolutionDir:  <DebuggeeBuildRoot>/<DebuggeeName>
+        //DebuggeeProjectDir:   <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]
+        //DebuggeeBinaryDir:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>]
+        //DebuggeeBinaryDll:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/<DebuggeeName>.dll
+        //DebuggeeBinaryExe:    <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>]/<DebuggeeName>.exe
+        //LogPath:              <DebuggeeBuildRoot>/<DebuggeeName>.txt
+
+        // When the runtime directory is present it will have a native host exe in it that has been renamed to the debugee
+        // name. It also has a managed dll in it which functions as a managed exe when renamed.
+        // When the runtime directory is missing, the framework directory will have a managed dll in it that functions if it
+        // is renamed to an exe. I'm sure that renaming isn't the intended usage, but it works and produces less churn
+        // in our tests for the moment.
+        public abstract DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName);
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs
new file mode 100644 (file)
index 0000000..c843ac1
--- /dev/null
@@ -0,0 +1,83 @@
+// 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 System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish.
+    /// </summary>
+    public class CliDebuggeeCompiler : BaseDebuggeeCompiler
+    {
+        /// <summary>
+        /// Creates a new CliDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish.
+               /// <param name="config">
+               ///   LinkerPackageVersion   If set, this version of the linker package will be used to link the debuggee during publish.
+               /// </param>
+        /// </summary>
+        public CliDebuggeeCompiler(TestConfiguration config, string debuggeeName) : base(config, debuggeeName) {}
+
+        private static Dictionary<string,string> GetBuildProperties(TestConfiguration config, string runtimeIdentifier)
+        {
+            Dictionary<string, string> buildProperties = new Dictionary<string, string>();
+            buildProperties.Add("RuntimeFrameworkVersion", config.BuildProjectMicrosoftNetCoreAppVersion);
+            buildProperties.Add("BuildProjectFramework", config.BuildProjectFramework);
+            if (runtimeIdentifier != null)
+            {
+                buildProperties.Add("RuntimeIdentifier", runtimeIdentifier);
+            }
+            string debugType = config.DebugType;
+            if (debugType == null)
+            {
+                // The default PDB type is portable
+                debugType = "portable";
+            }
+            buildProperties.Add("DebugType", debugType);
+            return buildProperties;
+        }
+
+        protected override string GetFramework(TestConfiguration config)
+        {
+            return config.BuildProjectFramework ?? "netcoreapp2.0";
+        }
+
+        protected override string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime)
+        {
+            string debuggeeBinaryDirPath = base.GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtime);
+            debuggeeBinaryDirPath = Path.Combine(debuggeeBinaryDirPath, "publish");
+            return debuggeeBinaryDirPath;
+        }
+
+        public override DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName)
+        {
+            string runtimeIdentifier = GetRuntime(config);
+            string framework = GetFramework(config);
+            string initialSourceDirPath = GetInitialSourceDirPath(config, debuggeeName);
+            string dotNetRootBuildDirPath = GetDotNetRootBuildDirPath(config);
+            string debuggeeSolutionDirPath = GetDebuggeeSolutionDirPath(dotNetRootBuildDirPath, debuggeeName);
+            string debuggeeProjectDirPath = GetDebuggeeProjectDirPath(debuggeeSolutionDirPath, initialSourceDirPath, debuggeeName);
+            string debuggeeBinaryDirPath = GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtimeIdentifier);
+            string debuggeeBinaryDllPath = config.IsNETCore ? GetDebuggeeBinaryDllPath(debuggeeBinaryDirPath, debuggeeName) : null;
+            string debuggeeBinaryExePath = config.IsDesktop ? GetDebuggeeBinaryExePath(debuggeeBinaryDirPath, debuggeeName) : null;
+            string logPath = GetLogPath(config, framework, runtimeIdentifier, debuggeeName);
+            return new CsprojBuildDebuggeeTestStep(dotNetPath,
+                                               initialSourceDirPath,
+                                               GetDebuggeeNativeLibDirPath(config, debuggeeName),
+                                               GetBuildProperties(config, runtimeIdentifier),
+                                               runtimeIdentifier,
+                                               config.LinkerPackageVersion,
+                                               debuggeeName,
+                                               debuggeeSolutionDirPath,
+                                               debuggeeProjectDirPath,
+                                               debuggeeBinaryDirPath,
+                                               debuggeeBinaryDllPath,
+                                               debuggeeBinaryExePath,
+                                               config.NuGetPackageCacheDir,
+                                               GetNugetFeeds(config),
+                                               logPath);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs
new file mode 100644 (file)
index 0000000..288d90e
--- /dev/null
@@ -0,0 +1,22 @@
+// 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 System;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public class ConsoleTestOutputHelper : ITestOutputHelper
+    {
+        public void WriteLine(string message)
+        {
+            Console.WriteLine(message);
+        }
+
+        public void WriteLine(string format, params object[] args)
+        {
+            Console.WriteLine(format, args);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs
new file mode 100644 (file)
index 0000000..7010938
--- /dev/null
@@ -0,0 +1,136 @@
+// 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 System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// This test step builds debuggees using the dotnet tools with .csproj projects files.
+    /// </summary>
+    /// <remarks>
+    /// Any <debuggee name>.csproj file that is found will be specialized by adding a linker package reference.
+    /// This lets us decide the runtime and dependency versions at test execution time.
+    /// </remarks>
+    public class CsprojBuildDebuggeeTestStep : DotNetBuildDebuggeeTestStep
+    {
+        /// <param name="buildProperties">
+        /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee.
+        /// </param>
+        /// <param name="runtimeIdentifier">
+        /// The runtime moniker to be built.
+        /// </param>
+        public CsprojBuildDebuggeeTestStep(string dotnetToolPath,
+                                       string templateSolutionDirPath,
+                                       string debuggeeNativeLibDirPath,
+                                       Dictionary<string,string> buildProperties,
+                                       string runtimeIdentifier,
+                                       string linkerPackageVersion,
+                                                          string debuggeeName,
+                                       string debuggeeSolutionDirPath,
+                                       string debuggeeProjectDirPath,
+                                       string debuggeeBinaryDirPath,
+                                       string debuggeeBinaryDllPath,
+                                       string debuggeeBinaryExePath,
+                                       string nugetPackageCacheDirPath,
+                                       Dictionary<string,string> nugetFeeds,
+                                       string logPath) :
+            base(dotnetToolPath,
+                 templateSolutionDirPath,
+                 debuggeeNativeLibDirPath,
+                 debuggeeSolutionDirPath,
+                 debuggeeProjectDirPath,
+                 debuggeeBinaryDirPath,
+                 debuggeeBinaryDllPath,
+                 debuggeeBinaryExePath,
+                 nugetPackageCacheDirPath,
+                 nugetFeeds,
+                 logPath)
+        {
+            BuildProperties = buildProperties;
+            RuntimeIdentifier = runtimeIdentifier;
+            DebuggeeName = debuggeeName;
+            ProjectTemplateFileName = debuggeeName + ".csproj";
+            LinkerPackageVersion = linkerPackageVersion;
+        }
+
+        /// <summary>
+        /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee.
+        /// </summary>
+        public IDictionary<string,string> BuildProperties { get; }
+        public string RuntimeIdentifier { get; }
+        public string DebuggeeName { get; }
+        public string LinkerPackageVersion { get; }
+        public override string ProjectTemplateFileName { get; }
+
+        protected override async Task Restore(ITestOutputHelper output)
+        {
+            string extraArgs = "";
+            if (RuntimeIdentifier != null)
+            {
+                extraArgs = " --runtime " + RuntimeIdentifier;
+            }
+            foreach (var prop in BuildProperties)
+            {
+                extraArgs += $" /p:{prop.Key}={prop.Value}";
+            }
+            await Restore(extraArgs, output);
+        }
+
+        protected override async Task Build(ITestOutputHelper output)
+        {
+            string publishArgs = "publish";
+            if (RuntimeIdentifier != null)
+            {
+                publishArgs += " --runtime " + RuntimeIdentifier;
+            }
+            foreach (var prop in BuildProperties)
+            {
+                publishArgs += $" /p:{prop.Key}={prop.Value}";
+            }
+            await Build(publishArgs, output);
+        }
+
+        protected override void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output)
+        {
+            ConvertCsprojTemplate(filePath, Path.Combine(destDirPath, DebuggeeName + ".csproj"));
+        }
+
+        private void ConvertCsprojTemplate(string csprojTemplatePath, string csprojOutPath)
+        {
+            var xdoc = XDocument.Load(csprojTemplatePath);
+            var ns = xdoc.Root.GetDefaultNamespace();
+            if (LinkerPackageVersion != null)
+            {
+                AddLinkerPackageReference(xdoc, ns, LinkerPackageVersion);
+            }
+            using (var fs = new FileStream(csprojOutPath, FileMode.Create))
+            {
+                xdoc.Save(fs);
+            }
+        }
+
+        private static void AddLinkerPackageReference(XDocument xdoc, XNamespace ns, string linkerPackageVersion)
+        {
+            xdoc.Root.Add(new XElement(ns + "ItemGroup",
+                                       new XElement(ns + "PackageReference",
+                                                    new XAttribute("Include", "ILLink.Tasks"),
+                                                    new XAttribute("Version", linkerPackageVersion))));
+        }
+
+        protected override void AssertDebuggeeAssetsFileExists(ITestOutputHelper output)
+        {
+            AssertX.FileExists("debuggee project.assets.json", Path.Combine(DebuggeeProjectDirPath, "obj", "project.assets.json"), output);
+        }
+
+        protected override void AssertDebuggeeProjectFileExists(ITestOutputHelper output)
+        {
+            AssertX.FileExists("debuggee csproj", Path.Combine(DebuggeeProjectDirPath, DebuggeeName + ".csproj"), output);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs
new file mode 100644 (file)
index 0000000..9dd2fce
--- /dev/null
@@ -0,0 +1,54 @@
+// 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 System;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// DebugeeCompiler is responsible for finding and/or producing the source and binaries of a given debuggee.
+    /// The steps it takes to do this depend on the TestConfiguration.
+    /// </summary>
+    public static class DebuggeeCompiler
+    {
+        async public static Task<DebuggeeConfiguration> Execute(TestConfiguration config, string debuggeeName, ITestOutputHelper output)
+        {
+            IDebuggeeCompiler compiler = null;
+            if (config.DebuggeeBuildProcess == "prebuilt")
+            {
+                compiler = new PrebuiltDebuggeeCompiler(config, debuggeeName);
+            }
+            else if (config.DebuggeeBuildProcess == "cli")
+            {
+                compiler = new CliDebuggeeCompiler(config, debuggeeName);
+            }
+            else
+            {
+                throw new Exception("Invalid DebuggeeBuildProcess configuration value. Expected 'prebuilt', actual \'" + config.DebuggeeBuildProcess + "\'");
+            }
+
+            return await compiler.Execute(output);
+        }
+    }
+
+    public interface IDebuggeeCompiler
+    {
+        Task<DebuggeeConfiguration> Execute(ITestOutputHelper output);
+    }
+
+    public class DebuggeeConfiguration
+    {
+        public DebuggeeConfiguration(string sourcePath, string binaryDirPath, string binaryExePath)
+        {
+            SourcePath = sourcePath;
+            BinaryDirPath = binaryDirPath;
+            BinaryExePath = binaryExePath;
+        }
+        public string SourcePath { get; private set; }
+        public string BinaryDirPath { get; private set; }
+        public string BinaryExePath { get; private set; }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs
new file mode 100644 (file)
index 0000000..6a3b735
--- /dev/null
@@ -0,0 +1,363 @@
+// 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 System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// This test step builds debuggees using the dotnet tools
+    /// </summary>
+    /// <remarks>
+    /// The build process consists of the following steps:
+    ///   1. Create a source directory with the conventions that dotnet expects by copying it from the DebuggeeSourceRoot. Any template project
+    ///      file that is found will be specialized by the implementation class.
+    ///   2. Run dotnet restore in the newly created source directory
+    ///   3. Run dotnet build in the same directory
+    ///   4. Rename the built debuggee dll to an exe (so that it conforms to some historical conventions our tests expect)
+    ///   5. Copy any native dependencies from the DebuggeeNativeLibRoot to the debuggee output directory
+    /// </remarks>
+    public abstract class DotNetBuildDebuggeeTestStep : TestStep
+    {
+        /// <summary>
+        /// Create a new DotNetBuildDebuggeeTestStep.
+        /// </summary>
+        /// <param name="dotnetToolPath">
+        /// The path to the dotnet executable
+        /// </param>
+        /// <param name="templateSolutionDirPath">
+        /// The path to the template solution source. This will be copied into the final solution source directory
+        /// under debuggeeSolutionDirPath and any project.template.json files it contains will be specialized.
+        /// </param>
+        /// <param name="debuggeeNativeLibDirPath">
+        /// The path where the debuggee's native binary dependencies will be copied from.
+        /// </param>
+        /// <param name="debuggeeSolutionDirPath">
+        /// The path where the debuggee solution will be created. For single project solutions this will be identical to
+        /// the debuggee project directory.
+        /// </param>
+        /// <param name="debuggeeProjectDirPath">
+        /// The path where the primary debuggee executable project directory will be created. For single project solutions this
+        /// will be identical to the debuggee solution directory.
+        /// </param>
+        /// <param name="debuggeeBinaryDirPath">
+        /// The directory path where the dotnet tool will place the compiled debuggee binaries.
+        /// </param>
+        /// <param name="debuggeeBinaryDllPath">
+        /// The path where the dotnet tool will place the compiled debuggee assembly.
+        /// </param>
+        /// <param name="debuggeeBinaryExePath">
+        /// The path to which the build will copy the debuggee binary dll with a .exe extension.
+        /// </param>
+        /// <param name="nugetPackageCacheDirPath">
+        /// The path to the NuGet package cache. If null, no value for this setting will be placed in the
+        /// NuGet.config file and dotnet will need to read it from other ambient NuGet.config files or infer
+        /// a default cache.
+        /// </param>
+        /// <param name="nugetFeeds">
+        /// A mapping of nuget feed names to locations. These feeds will be used to restore debuggee
+        /// nuget package dependencies.
+        /// </param>
+        /// <param name="logPath">
+        /// The path where the build output will be logged
+        /// </param>
+        public DotNetBuildDebuggeeTestStep(string dotnetToolPath,
+                                       string templateSolutionDirPath,
+                                       string debuggeeNativeLibDirPath,
+                                       string debuggeeSolutionDirPath,
+                                       string debuggeeProjectDirPath,
+                                       string debuggeeBinaryDirPath,
+                                       string debuggeeBinaryDllPath,
+                                       string debuggeeBinaryExePath,
+                                       string nugetPackageCacheDirPath,
+                                       Dictionary<string,string> nugetFeeds,
+                                       string logPath) :
+            base(logPath, "Build Debuggee") 
+        {
+            DotNetToolPath = dotnetToolPath;
+            DebuggeeTemplateSolutionDirPath = templateSolutionDirPath;
+            DebuggeeNativeLibDirPath = debuggeeNativeLibDirPath;
+            DebuggeeSolutionDirPath = debuggeeSolutionDirPath;
+            DebuggeeProjectDirPath = debuggeeProjectDirPath;
+            DebuggeeBinaryDirPath = debuggeeBinaryDirPath;
+            DebuggeeBinaryDllPath = debuggeeBinaryDllPath;
+            DebuggeeBinaryExePath = debuggeeBinaryExePath;
+            NuGetPackageCacheDirPath = nugetPackageCacheDirPath;
+            NugetFeeds = nugetFeeds;
+            if(NugetFeeds != null && NugetFeeds.Count > 0)
+            {
+                NuGetConfigPath = Path.Combine(DebuggeeSolutionDirPath, "NuGet.config");
+            }
+        }
+
+        /// <summary>
+        /// The path to the dotnet executable
+        /// </summary>
+        public string DotNetToolPath { get; private set; }
+        /// <summary>
+        /// The path to the template solution source. This will be copied into the final solution source directory
+        /// under debuggeeSolutionDirPath and any project.template.json files it contains will be specialized.
+        /// </summary>
+        public string DebuggeeTemplateSolutionDirPath { get; private set; }
+        /// <summary>
+        /// The path where the debuggee's native binary dependencies will be copied from.
+        /// </summary>
+        public string DebuggeeNativeLibDirPath { get; private set; }
+        /// <summary>
+        /// The path where the debuggee solution will be created. For single project solutions this will be identical to
+        /// the debuggee project directory.
+        /// </summary>
+        public string DebuggeeSolutionDirPath { get; private set; }
+        /// <summary>
+        /// The path where the primary debuggee executable project directory will be created. For single project solutions this
+        /// will be identical to the debuggee solution directory.
+        /// </summary>
+        public string DebuggeeProjectDirPath { get; private set; }
+        /// <summary>
+        /// The directory path where the dotnet tool will place the compiled debuggee binaries.
+        /// </summary>
+        public string DebuggeeBinaryDirPath { get; private set; }
+        /// <summary>
+        /// The path where the dotnet tool will place the compiled debuggee assembly.
+        /// </summary>
+        public string DebuggeeBinaryDllPath { get; private set; }
+        /// <summary>
+        /// The path to which the build will copy the debuggee binary dll with a .exe extension.
+        /// </summary>
+        public string DebuggeeBinaryExePath { get; private set; }
+        /// The path to the NuGet package cache. If null, no value for this setting will be placed in the
+        /// NuGet.config file and dotnet will need to read it from other ambient NuGet.config files or infer
+        /// a default cache.
+        public string NuGetPackageCacheDirPath { get; private set; }
+        public string NuGetConfigPath { get; private set; }
+        public IDictionary<string,string> NugetFeeds { get; private set; }
+        public abstract string ProjectTemplateFileName { get; }
+
+        async protected override Task DoWork(ITestOutputHelper output)
+        {
+            PrepareProjectSolution(output);
+            await Restore(output);
+            await Build(output);
+            CopyNativeDependencies(output);
+        }
+
+        void PrepareProjectSolution(ITestOutputHelper output)
+        {
+            AssertDebuggeeSolutionTemplateDirExists(output);
+
+            output.WriteLine("Creating Solution Source Directory");
+            output.WriteLine("{");
+            IndentedTestOutputHelper indentedOutput = new IndentedTestOutputHelper(output);
+            CopySourceDirectory(DebuggeeTemplateSolutionDirPath, DebuggeeSolutionDirPath, indentedOutput);
+            CreateNuGetConfig(indentedOutput);
+            output.WriteLine("}");
+            output.WriteLine("");
+
+            AssertDebuggeeSolutionDirExists(output);
+            AssertDebuggeeProjectDirExists(output);
+            AssertDebuggeeProjectFileExists(output);
+        }
+
+        SemaphoreSlim _dotnetRestoreLock = new SemaphoreSlim(1);
+
+        protected async Task Restore(string extraArgs, ITestOutputHelper output)
+        {
+            AssertDebuggeeSolutionDirExists(output);
+            AssertDebuggeeProjectDirExists(output);
+            AssertDebuggeeProjectFileExists(output);
+
+            string args = "restore";
+            if (NuGetConfigPath != null)
+            {
+                args += " --configfile " + NuGetConfigPath;
+            }
+            if (NuGetPackageCacheDirPath != null)
+            {
+                args += " --packages \"" + NuGetPackageCacheDirPath + "\"";
+            }
+            if (extraArgs != null)
+            {
+                args += extraArgs;
+            }
+            ProcessRunner runner = new ProcessRunner(DotNetToolPath, args).
+                      WithWorkingDirectory(DebuggeeSolutionDirPath).
+                      WithLog(output).
+                      WithTimeout(TimeSpan.FromMinutes(10)).                    // restore can be painfully slow
+                      WithExpectedExitCode(0);
+
+            if (OS.Kind != OSKind.Windows && Environment.GetEnvironmentVariable("HOME") == null)
+            {
+                output.WriteLine("Detected HOME environment variable doesn't exist. This will trigger a bug in dotnet restore.");
+                output.WriteLine("See: https://github.com/NuGet/Home/issues/2960");
+                output.WriteLine("Test will workaround this by manually setting a HOME value");
+                output.WriteLine("");
+                runner = runner.WithEnvironmentVariable("HOME", DebuggeeSolutionDirPath);
+            }
+
+            //workaround for https://github.com/dotnet/cli/issues/3868
+            await _dotnetRestoreLock.WaitAsync();
+            try
+            {
+                await runner.Run();
+            }
+            finally
+            {
+                _dotnetRestoreLock.Release();
+            }
+
+            AssertDebuggeeAssetsFileExists(output);
+        }
+
+        protected virtual async Task Restore(ITestOutputHelper output)
+        {
+            await Restore(null, output);
+        }
+
+        protected async Task Build(string dotnetArgs, ITestOutputHelper output)
+        {
+            AssertDebuggeeSolutionDirExists(output);
+            AssertDebuggeeProjectFileExists(output);
+            AssertDebuggeeAssetsFileExists(output);
+
+            ProcessRunner runner = new ProcessRunner(DotNetToolPath, dotnetArgs).
+                      WithWorkingDirectory(DebuggeeProjectDirPath).
+                      WithLog(output).
+                      WithTimeout(TimeSpan.FromMinutes(10)). // a mac CI build of the modules debuggee is painfully slow :(
+                      WithExpectedExitCode(0);
+
+            if (OS.Kind != OSKind.Windows && Environment.GetEnvironmentVariable("HOME") == null)
+            {
+                output.WriteLine("Detected HOME environment variable doesn't exist. This will trigger a bug in dotnet build.");
+                output.WriteLine("See: https://github.com/NuGet/Home/issues/2960");
+                output.WriteLine("Test will workaround this by manually setting a HOME value");
+                output.WriteLine("");
+                runner = runner.WithEnvironmentVariable("HOME", DebuggeeSolutionDirPath);
+            }
+            if (NuGetPackageCacheDirPath != null)
+            {
+                //dotnet restore helpfully documents its --packages argument in the help text, but
+                //NUGET_PACKAGES was undocumented as far as I noticed. If this stops working we can also
+                //auto-generate a global.json with a "packages" setting, but this was more expedient.
+                runner = runner.WithEnvironmentVariable("NUGET_PACKAGES", NuGetPackageCacheDirPath);
+            }
+
+            await runner.Run();
+
+            if (DebuggeeBinaryExePath != null)
+            {
+                AssertDebuggeeExeExists(output);
+            }
+            else
+            {
+                AssertDebuggeeDllExists(output);
+            }
+        }
+
+        protected virtual async Task Build(ITestOutputHelper output)
+        {
+            await Build("build", output);
+        }
+
+        void CopyNativeDependencies(ITestOutputHelper output)
+        {
+            if (Directory.Exists(DebuggeeNativeLibDirPath))
+            {
+                foreach (string filePath in Directory.EnumerateFiles(DebuggeeNativeLibDirPath))
+                {
+                    string targetPath = Path.Combine(DebuggeeBinaryDirPath, Path.GetFileName(filePath));
+                    output.WriteLine("Copying: " + filePath + " -> " + targetPath);
+                    File.Copy(filePath, targetPath);
+                }
+            }
+        }
+
+        private void CopySourceDirectory(string sourceDirPath, string destDirPath, ITestOutputHelper output)
+        {
+            output.WriteLine("Copying: " + sourceDirPath + " -> " + destDirPath);
+            Directory.CreateDirectory(destDirPath);
+            foreach(string dirPath in Directory.EnumerateDirectories(sourceDirPath))
+            {
+                CopySourceDirectory(dirPath, Path.Combine(destDirPath, Path.GetFileName(dirPath)), output);
+            }
+            foreach (string filePath in Directory.EnumerateFiles(sourceDirPath))
+            {
+                string fileName = Path.GetFileName(filePath);
+                if (fileName == ProjectTemplateFileName)
+                {
+                    ExpandProjectTemplate(filePath, destDirPath, output);
+                }
+                else
+                {
+                    File.Copy(filePath, Path.Combine(destDirPath, Path.GetFileName(filePath)), true);
+                }
+            }
+        }
+
+        protected abstract void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output);
+
+        protected void CreateNuGetConfig(ITestOutputHelper output)
+        {
+            if (NuGetConfigPath == null)
+            {
+                return;
+            }
+            string nugetConfigPath = Path.Combine(DebuggeeSolutionDirPath, "NuGet.config");
+            StringBuilder sb = new StringBuilder();
+            sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
+            sb.AppendLine("<configuration>");
+            if(NugetFeeds != null && NugetFeeds.Count > 0)
+            {
+                sb.AppendLine("  <packageSources>");
+                sb.AppendLine("    <clear />");
+                foreach(KeyValuePair<string, string> kv in NugetFeeds)
+                {
+                    sb.AppendLine("    <add key=\"" + kv.Key + "\" value=\"" + kv.Value + "\" />");
+                }
+                sb.AppendLine("  </packageSources>");
+                sb.AppendLine("  <activePackageSource>");
+                sb.AppendLine("    <add key=\"All\" value=\"(Aggregate source)\" />");
+                sb.AppendLine("  </activePackageSource>");
+            }
+            sb.AppendLine("</configuration>");
+
+            output.WriteLine("Creating: " + NuGetConfigPath);
+            File.WriteAllText(NuGetConfigPath, sb.ToString());
+        }
+
+        protected void AssertDebuggeeSolutionTemplateDirExists(ITestOutputHelper output)
+        {
+            AssertX.DirectoryExists("debuggee solution template directory", DebuggeeTemplateSolutionDirPath, output);
+        }
+
+        protected void AssertDebuggeeProjectDirExists(ITestOutputHelper output)
+        {
+            AssertX.DirectoryExists("debuggee project directory", DebuggeeProjectDirPath, output);
+        }
+
+        protected void AssertDebuggeeSolutionDirExists(ITestOutputHelper output)
+        {
+            AssertX.DirectoryExists("debuggee solution directory", DebuggeeSolutionDirPath, output);
+        }
+
+        protected void AssertDebuggeeDllExists(ITestOutputHelper output)
+        {
+            AssertX.FileExists("debuggee dll", DebuggeeBinaryDllPath, output);
+        }
+
+        protected void AssertDebuggeeExeExists(ITestOutputHelper output)
+        {
+            AssertX.FileExists("debuggee exe", DebuggeeBinaryExePath, output);
+        }
+
+        protected abstract void AssertDebuggeeAssetsFileExists(ITestOutputHelper output);
+
+        protected abstract void AssertDebuggeeProjectFileExists(ITestOutputHelper output);
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs
new file mode 100644 (file)
index 0000000..1f4a398
--- /dev/null
@@ -0,0 +1,52 @@
+// 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 System;
+using System.IO;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// An ITestOutputHelper implementation that logs to a file
+    /// </summary>
+    public class FileTestOutputHelper : ITestOutputHelper, IDisposable
+    {
+        readonly StreamWriter _logWriter;
+        readonly object _lock;
+
+        public FileTestOutputHelper(string logFilePath, FileMode fileMode = FileMode.Create)
+        {
+            Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
+            FileStream fs = new FileStream(logFilePath, fileMode);
+            _logWriter = new StreamWriter(fs);
+            _logWriter.AutoFlush = true;
+            _lock = new object();
+        }
+
+        public void WriteLine(string message)
+        {
+            lock (_lock)
+            {
+                _logWriter.WriteLine(message);
+            }
+        }
+
+        public void WriteLine(string format, params object[] args)
+        {
+            lock (_lock)
+            {
+                _logWriter.WriteLine(format, args);
+            }
+        }
+
+        public void Dispose()
+        {
+            lock (_lock)
+            {
+                _logWriter.Dispose();
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs b/src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs
new file mode 100644 (file)
index 0000000..233372a
--- /dev/null
@@ -0,0 +1,29 @@
+// 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.
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public enum ProcessStream
+    {
+        StandardIn = 0,
+        StandardOut = 1,
+        StandardError = 2,
+        MaxStreams = 3
+    }
+
+    public enum KillReason
+    {
+        TimedOut,
+        Unknown
+    }
+
+    public interface IProcessLogger
+    {
+        void ProcessExited(ProcessRunner runner);
+        void ProcessKilled(ProcessRunner runner, KillReason reason);
+        void ProcessStarted(ProcessRunner runner);
+        void Write(ProcessRunner runner, string data, ProcessStream stream);
+        void WriteLine(ProcessRunner runner, string data, ProcessStream stream);
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs
new file mode 100644 (file)
index 0000000..1a2a695
--- /dev/null
@@ -0,0 +1,34 @@
+// 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.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// An implementation of ITestOutputHelper that adds one indent level to
+    /// the start of each line
+    /// </summary>
+    public class IndentedTestOutputHelper : ITestOutputHelper
+    {
+        readonly string _indentText;
+        readonly ITestOutputHelper _output;
+
+        public IndentedTestOutputHelper(ITestOutputHelper innerOutput, string indentText = "    ")
+        {
+            _output = innerOutput;
+            _indentText = indentText;
+        }
+
+        public void WriteLine(string message)
+        {
+            _output.WriteLine(_indentText + message);
+        }
+
+        public void WriteLine(string format, params object[] args)
+        {
+            _output.WriteLine(_indentText + format, args);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj b/src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj
new file mode 100644 (file)
index 0000000..0800c15
--- /dev/null
@@ -0,0 +1,18 @@
+<!-- Copyright (c)  Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information. -->
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <NoWarn>;1591;1701</NoWarn>
+    <IsPackable>true</IsPackable>
+    <Description>Diagnostic test support</Description>
+    <PackageReleaseNotes>$(Description)</PackageReleaseNotes>
+    <PackageTags>tests</PackageTags>
+    <DebugType>embedded</DebugType>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <PackageReference Include="xunit" Version="$(XUnitVersion)" />
+    <PackageReference Include="xunit.abstractions" Version="$(XUnitAbstractionsVersion)" />
+  </ItemGroup>
+</Project>
diff --git a/src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs
new file mode 100644 (file)
index 0000000..6814a10
--- /dev/null
@@ -0,0 +1,34 @@
+// 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.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public class MultiplexTestOutputHelper : ITestOutputHelper
+    {
+        readonly ITestOutputHelper[] _outputs;
+
+        public MultiplexTestOutputHelper(params ITestOutputHelper[] outputs)
+        {
+            _outputs = outputs;
+        }
+
+        public void WriteLine(string message)
+        {
+            foreach(ITestOutputHelper output in _outputs)
+            {
+                output.WriteLine(message);
+            }
+        }
+
+        public void WriteLine(string format, params object[] args)
+        {
+            foreach (ITestOutputHelper output in _outputs)
+            {
+                output.WriteLine(format, args);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs
new file mode 100644 (file)
index 0000000..0c6cb51
--- /dev/null
@@ -0,0 +1,38 @@
+// 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 System.IO;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public class PrebuiltDebuggeeCompiler : IDebuggeeCompiler
+    {
+        string _sourcePath;
+        string _binaryPath;
+        string _binaryExePath;
+
+        public PrebuiltDebuggeeCompiler(TestConfiguration config, string debuggeeName)
+        {
+            //we anticipate paths like this:
+            //Source:   <DebuggeeSourceRoot>/<DebuggeeName>/[<DebuggeeName>]
+            //Binaries: <DebuggeeBuildRoot>/<DebuggeeName>/
+            _sourcePath = Path.Combine(config.DebuggeeSourceRoot, debuggeeName);
+            if (Directory.Exists(Path.Combine(_sourcePath, debuggeeName)))
+            {
+                _sourcePath = Path.Combine(_sourcePath, debuggeeName);
+            }
+
+            _binaryPath = Path.Combine(config.DebuggeeBuildRoot, debuggeeName);
+            _binaryExePath = Path.Combine(_binaryPath, debuggeeName);
+            _binaryExePath += ".exe";
+        }
+
+        public Task<DebuggeeConfiguration> Execute(ITestOutputHelper output)
+        {
+            return Task.Factory.StartNew<DebuggeeConfiguration>(() => new DebuggeeConfiguration(_sourcePath, _binaryPath, _binaryExePath));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs b/src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs
new file mode 100644 (file)
index 0000000..5dad0e5
--- /dev/null
@@ -0,0 +1,469 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// Executes a process and logs the output
+    /// </summary>
+    /// <remarks>
+    /// The intended lifecycle is:
+    ///   a) Create a new ProcessRunner
+    ///   b) Use the various WithXXX methods to modify the configuration of the process to launch
+    ///   c) await RunAsync() to start the process and wait for it to terminate. Configuration
+    ///      changes are no longer possible
+    ///   d) While waiting for RunAsync(), optionally call Kill() one or more times. This will expedite 
+    ///      the termination of the process but there is no guarantee the process is terminated by
+    ///      the time Kill() returns.
+    ///      
+    ///   Although the entire API of this type has been designed to be thread-safe, its typical that
+    ///   only calls to Kill() and property getters invoked within the logging callbacks will be called
+    ///   asynchronously.
+    /// </remarks>
+    public class ProcessRunner
+    {
+        // All of the locals might accessed from multiple threads and need to read/written under
+        // the _lock. We also use the lock to synchronize property access on the process object.
+        //
+        // Be careful not to cause deadlocks by calling the logging callbacks with the lock held.
+        // The logger has its own lock and it will hold that lock when it calls into property getters
+        // on this type.
+        object _lock = new object();
+
+        List<IProcessLogger> _loggers;
+        Process _p;
+        DateTime _startTime;
+        TimeSpan _timeout;
+        ITestOutputHelper _traceOutput;
+        int? _expectedExitCode;
+        TaskCompletionSource<Process> _waitForProcessStartTaskSource;
+        Task<int> _waitForExitTask;
+        Task _timeoutProcessTask;
+        Task _readStdOutTask;
+        Task _readStdErrTask;
+        CancellationTokenSource _cancelSource;
+        private string _replayCommand;
+        private KillReason? _killReason;
+
+        public ProcessRunner(string exePath, string arguments, string replayCommand = null)
+        {
+            ProcessStartInfo psi = new ProcessStartInfo();
+            psi.FileName = exePath;
+            psi.Arguments = arguments;
+            psi.UseShellExecute = false;
+            psi.RedirectStandardInput = true;
+            psi.RedirectStandardOutput = true;
+            psi.RedirectStandardError = true;
+            psi.CreateNoWindow = true;
+
+            lock (_lock)
+            {
+                _p = new Process();
+                _p.StartInfo = psi;
+                _p.EnableRaisingEvents = false;
+                _loggers = new List<IProcessLogger>();
+                _timeout = TimeSpan.FromMinutes(10);
+                _cancelSource = new CancellationTokenSource();
+                _killReason = null;
+                _waitForProcessStartTaskSource = new TaskCompletionSource<Process>();
+                Task<Process> startTask = _waitForProcessStartTaskSource.Task;
+                
+                // unfortunately we can't use the default Process stream reading because it only returns full lines and we have scenarios
+                // that need to receive the output before the newline character is written
+                _readStdOutTask = startTask.ContinueWith(t =>
+                {
+                    ReadStreamToLoggers(_p.StandardOutput, ProcessStream.StandardOut, _cancelSource.Token);
+                }, 
+                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
+
+                _readStdErrTask = startTask.ContinueWith(t =>
+                {
+                    ReadStreamToLoggers(_p.StandardError, ProcessStream.StandardError, _cancelSource.Token);
+                }, 
+                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
+
+                _timeoutProcessTask = startTask.ContinueWith(t =>
+                {
+                    Task.Delay(_timeout, _cancelSource.Token).ContinueWith(t2 => Kill(KillReason.TimedOut), TaskContinuationOptions.NotOnCanceled);
+                },
+                _cancelSource.Token, TaskContinuationOptions.LongRunning, TaskScheduler.Default);
+
+                _waitForExitTask = InternalWaitForExit(startTask, _readStdOutTask, _readStdErrTask);
+                
+                if (replayCommand == null)
+                {
+                    _replayCommand = ExePath + " " + Arguments;
+                }
+                else
+                {
+                    _replayCommand = replayCommand;
+                }
+            }
+        }
+        
+        public string ReplayCommand
+        {
+            get { lock (_lock) { return _replayCommand; } }
+        }
+
+        public ProcessRunner WithEnvironmentVariable(string key, string value)
+        {
+            lock (_lock)
+            {
+                _p.StartInfo.Environment[key] = value;
+            }
+            return this;
+        }
+
+        public ProcessRunner WithWorkingDirectory(string workingDirectory)
+        {
+            lock (_lock)
+            {
+                _p.StartInfo.WorkingDirectory = workingDirectory;
+            }
+            return this;
+        }
+
+        public ProcessRunner WithLog(IProcessLogger logger)
+        {
+            lock (_lock)
+            {
+                _loggers.Add(logger);
+            }
+            return this;
+        }
+
+        public ProcessRunner WithLog(ITestOutputHelper output)
+        {
+            lock (_lock)
+            {
+                _loggers.Add(new TestOutputProcessLogger(output));
+            }
+            return this;
+        }
+
+        public ProcessRunner WithDiagnosticTracing(ITestOutputHelper traceOutput)
+        {
+            lock (_lock)
+            {
+                _traceOutput = new ConsoleTestOutputHelper(traceOutput);
+            }
+            return this;
+        }
+
+        public IProcessLogger[] Loggers
+        {
+            get { lock (_lock) { return _loggers.ToArray(); } }
+        }
+
+        public ProcessRunner WithTimeout(TimeSpan timeout)
+        {
+            lock (_lock)
+            {
+                _timeout = timeout;
+            }
+            return this;
+        }
+
+        public ProcessRunner WithExpectedExitCode(int expectedExitCode)
+        {
+            lock (_lock)
+            {
+                _expectedExitCode = expectedExitCode;
+            }
+            return this;
+        }
+
+        public string ExePath
+        {
+            get { lock (_lock) { return _p.StartInfo.FileName; } }
+        }
+
+        public string Arguments
+        {
+            get { lock (_lock) { return _p.StartInfo.Arguments; } }
+        }
+
+        public string WorkingDirectory
+        {
+            get { lock (_lock) { return _p.StartInfo.WorkingDirectory; } }
+        }
+
+        public int ProcessId
+        {
+            get { lock (_lock) { return _p.Id; } }
+        }
+
+        public Dictionary<string,string> EnvironmentVariables
+        {
+            get { lock (_lock) { return new Dictionary<string, string>(_p.StartInfo.Environment); } }
+        }
+
+        public bool IsStarted
+        {
+            get { lock (_lock) { return _waitForProcessStartTaskSource.Task.IsCompleted; } }
+        }
+
+        public DateTime StartTime
+        {
+            get { lock (_lock) { return _startTime; } }
+        }
+
+        public int ExitCode
+        {
+            get { lock (_lock) { return _p.ExitCode; } }
+        }
+
+        public void StandardInputWriteLine(string line)
+        {
+            IProcessLogger[] loggers = null;
+            StreamWriter inputStream = null;
+            lock (_lock)
+            {
+                loggers = _loggers.ToArray();
+                inputStream = _p.StandardInput;
+            }
+            foreach (IProcessLogger logger in loggers)
+            {
+                logger.WriteLine(this, line, ProcessStream.StandardIn);
+            }
+            inputStream.WriteLine(line);
+        }
+
+        public Task<int> Run()
+        {
+            Start();
+            return WaitForExit();
+        }
+
+        public Task<int> WaitForExit()
+        {
+            lock (_lock)
+            {
+                return _waitForExitTask;
+            }
+        }
+
+        public ProcessRunner Start()
+        {
+            Process p = null;
+            lock (_lock)
+            {
+                p = _p;
+            }
+            // this is safe to call on multiple threads, it only launches the process once
+            bool started = p.Start();
+
+            IProcessLogger[] loggers = null;
+            lock (_lock)
+            {
+                // only the first thread to get here will initialize this state
+                if (!_waitForProcessStartTaskSource.Task.IsCompleted)
+                {
+                    loggers = _loggers.ToArray();
+                    _startTime = DateTime.Now;
+                    _waitForProcessStartTaskSource.SetResult(_p);
+                }
+            }
+
+            // only the first thread that entered the lock above will run this
+            if (loggers != null)
+            {
+                foreach (IProcessLogger logger in loggers)
+                {
+                    logger.ProcessStarted(this);
+                }
+            }
+
+            return this;
+        }
+
+        private void ReadStreamToLoggers(StreamReader reader, ProcessStream stream, CancellationToken cancelToken)
+        {
+            IProcessLogger[] loggers = Loggers;
+
+            // for the best efficiency we want to read in chunks, but if the underlying stream isn't
+            // going to timeout partial reads then we have to fall back to reading one character at a time
+            int readChunkSize = 1;
+            if (reader.BaseStream.CanTimeout)
+            {
+                readChunkSize = 1000;
+            }
+
+            char[] buffer = new char[readChunkSize];
+            bool lastCharWasCarriageReturn = false;
+            do
+            {
+                int charsRead = 0;
+                int lastStartLine = 0;
+                charsRead = reader.ReadBlock(buffer, 0, readChunkSize);
+
+                // this lock keeps the standard out/error streams from being intermixed
+                lock (loggers)
+                {
+                    for (int i = 0; i < charsRead; i++)
+                    {
+                        // eat the \n after a \r, if any
+                        bool isNewLine = buffer[i] == '\n';
+                        bool isCarriageReturn = buffer[i] == '\r';
+                        if (lastCharWasCarriageReturn && isNewLine)
+                        {
+                            lastStartLine++;
+                            lastCharWasCarriageReturn = false;
+                            continue;
+                        }
+                        lastCharWasCarriageReturn = isCarriageReturn;
+                        if (isCarriageReturn || isNewLine)
+                        {
+                            string line = new string(buffer, lastStartLine, i - lastStartLine);
+                            lastStartLine = i + 1;
+                            foreach (IProcessLogger logger in loggers)
+                            {
+                                logger.WriteLine(this, line, stream);
+                            }
+                        }
+                    }
+
+                    // flush any fractional line
+                    if (charsRead > lastStartLine)
+                    {
+                        string line = new string(buffer, lastStartLine, charsRead - lastStartLine);
+                        foreach (IProcessLogger logger in loggers)
+                        {
+                            logger.Write(this, line, stream);
+                        }
+                    }
+                }
+            }
+            while (!reader.EndOfStream && !cancelToken.IsCancellationRequested);
+        }
+
+        public void Kill(KillReason reason = KillReason.Unknown)
+        {
+            IProcessLogger[] loggers = null;
+            Process p = null;
+            lock (_lock)
+            {
+                if (_waitForExitTask.IsCompleted)
+                {
+                    return;
+                }
+                if (_killReason.HasValue)
+                {
+                    return;
+                }
+                _killReason = reason;
+                if (!_p.HasExited)
+                {
+                    p = _p;
+                }
+
+                loggers = _loggers.ToArray();
+                _cancelSource.Cancel();
+            }
+
+            if (p != null)
+            {
+                // its possible the process could exit just after we check so
+                // we still have to handle the InvalidOperationException that
+                // can be thrown.
+                try
+                {
+                    p.Kill();
+                }
+                catch (InvalidOperationException) { }
+            }
+
+            foreach (IProcessLogger logger in loggers)
+            {
+                logger.ProcessKilled(this, reason);
+            }
+        }
+
+        private async Task<int> InternalWaitForExit(Task<Process> startProcessTask, Task stdOutTask, Task stdErrTask)
+        {
+            DebugTrace("starting InternalWaitForExit");
+            Process p = await startProcessTask;
+            DebugTrace("InternalWaitForExit {0} '{1}'", p.Id, _replayCommand);
+
+            Task processExit = Task.Factory.StartNew(() =>
+            {
+                DebugTrace("starting Process.WaitForExit {0}", p.Id);
+                p.WaitForExit();
+                DebugTrace("ending Process.WaitForExit {0}", p.Id);
+            },
+            TaskCreationOptions.LongRunning);
+
+            DebugTrace("awaiting process {0} exit, stdOut, and stdErr", p.Id);
+            await Task.WhenAll(processExit, stdOutTask, stdErrTask);
+            DebugTrace("await process {0} exit, stdOut, and stdErr complete", p.Id);
+
+            foreach (IProcessLogger logger in Loggers)
+            {
+                logger.ProcessExited(this);
+            }
+
+            lock (_lock)
+            {
+                if (_expectedExitCode.HasValue && p.ExitCode != _expectedExitCode.Value)
+                {
+                    throw new Exception("Process returned exit code " + p.ExitCode + ", expected " + _expectedExitCode.Value + Environment.NewLine +
+                                        "Command Line: " + ReplayCommand + Environment.NewLine +
+                                        "Working Directory: " + WorkingDirectory);
+                }
+                DebugTrace("InternalWaitForExit {0} returning {1}", p.Id, p.ExitCode);
+                return p.ExitCode;
+            }
+        }
+
+        private void DebugTrace(string format, params object[] args)
+        {
+            lock (_lock)
+            {
+                if (_traceOutput != null)
+                {
+                    string message = string.Format(format, args);
+                    _traceOutput.WriteLine("TRACE: {0}", message);
+                }
+            }
+        }
+
+        class ConsoleTestOutputHelper : ITestOutputHelper
+        {
+            readonly ITestOutputHelper _output;
+
+            public ConsoleTestOutputHelper(ITestOutputHelper output)
+            {
+                _output = output;
+            }
+
+            public void WriteLine(string message)
+            {
+                Console.WriteLine(message);
+                if (_output != null)
+                {
+                    _output.WriteLine(message);
+                }
+
+            }
+
+            public void WriteLine(string format, params object[] args)
+            {
+                Console.WriteLine(format, args);
+                if (_output != null)
+                {
+                    _output.WriteLine(format, args);
+                }
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs b/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs
new file mode 100644 (file)
index 0000000..98c16a3
--- /dev/null
@@ -0,0 +1,702 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Xml.Linq;
+using Xunit;
+using Xunit.Extensions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// Represents the all the test configurations for a test run.
+    /// </summary>
+    public class TestRunConfiguration : IDisposable
+    {
+        public static TestRunConfiguration Instance
+        {
+            get { return _instance.Value; }
+        }
+
+        static Lazy<TestRunConfiguration> _instance = new Lazy<TestRunConfiguration>(() => ParseDefaultConfigFile());
+
+        static TestRunConfiguration ParseDefaultConfigFile()
+        {
+            string configFilePath = Path.Combine(TestConfiguration.BaseDir, "Debugger.Tests.Config.txt");
+            TestRunConfiguration testRunConfig = new TestRunConfiguration();
+            testRunConfig.ParseConfigFile(configFilePath);
+            return testRunConfig;
+        }
+
+        DateTime _timestamp = DateTime.Now;
+
+        public IEnumerable<TestConfiguration> Configurations { get; private set; }
+
+        void ParseConfigFile(string path)
+        {
+            string nugetPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
+            if (nugetPackages == null)
+            {
+                // If not already set, the arcade SDK scripts/build system sets NUGET_PACKAGES 
+                // to the UserProfile or HOME nuget cache directories if building locally (for 
+                // speed) or to the repo root/.packages in CI builds (to isolate global machine 
+                // dependences).
+                //
+                // This emulates that logic so the VS Test Explorer can still run the tests for
+                // config files that don't set the NugetPackagesCacheDir value (like the SOS unit
+                // tests).
+                string nugetPackagesRoot = null;
+                if (OS.Kind == OSKind.Windows)
+                {
+                    nugetPackagesRoot = Environment.GetEnvironmentVariable("UserProfile");
+                }
+                else if (OS.Kind == OSKind.Linux || OS.Kind == OSKind.OSX)
+                {
+                    nugetPackagesRoot = Environment.GetEnvironmentVariable("HOME");
+                }
+                if (nugetPackagesRoot != null)
+                {
+                    nugetPackages = Path.Combine(nugetPackagesRoot, ".nuget", "packages");
+                }
+            }
+            // The TargetArchitecture and NuGetPackageCacheDir can still be overridden
+            // in a config file. This is just setting the default. The other values can 
+            // also // be overridden but it is not recommended.
+            Dictionary<string, string> initialConfig = new Dictionary<string, string>
+            {
+                ["Timestamp"] = GetTimeStampText(),
+                ["TempPath"] = Path.GetTempPath(),
+                ["WorkingDir"] = GetInitialWorkingDir(),
+                ["OS"] = OS.Kind.ToString(),
+                ["TargetArchitecture"] = OS.TargetArchitecture.ToString().ToLowerInvariant(),
+                ["NuGetPackageCacheDir"] = nugetPackages
+            };
+            if (OS.Kind == OSKind.Windows)
+            {
+                initialConfig["WinDir"] = Path.GetFullPath(Environment.GetEnvironmentVariable("WINDIR"));
+            }
+            IEnumerable<Dictionary<string, string>> configs = ParseConfigFile(path, new Dictionary<string, string>[] { initialConfig });
+            Configurations = configs.Select(c => new TestConfiguration(c));
+        }
+
+        Dictionary<string, string>[] ParseConfigFile(string path, Dictionary<string, string>[] templates)
+        {
+            XDocument doc = XDocument.Load(path);
+            XElement elem = doc.Root;
+            Assert.Equal("Configuration", elem.Name);
+            return ParseConfigSettings(templates, elem);
+        }
+
+        string GetTimeStampText()
+        {
+            return _timestamp.ToString("yyyy\\_MM\\_dd\\_hh\\_mm\\_ss\\_ffff");
+        }
+
+        string GetInitialWorkingDir()
+        {
+            return Path.Combine(Path.GetTempPath(), "TestRun_" + GetTimeStampText());
+        }
+
+        Dictionary<string, string>[] ParseConfigSettings(Dictionary<string, string>[] templates, XElement node)
+        {
+            Dictionary<string, string>[] currentTemplates = templates;
+            foreach (XElement child in node.Elements())
+            {
+                currentTemplates = ParseConfigSetting(currentTemplates, child);
+            }
+            return currentTemplates;
+        }
+
+        Dictionary<string, string>[] ParseConfigSetting(Dictionary<string, string>[] templates, XElement node)
+        {
+            // As long as the templates are added at the end of the list, the "current" 
+            // config for this section is the last one in the array.
+            Dictionary<string, string> currentTemplate = templates.Last();
+
+            switch (node.Name.LocalName)
+            { 
+                case "Options":
+                    if (EvaluateConditional(currentTemplate, node))
+                    {
+                        List<Dictionary<string, string>> newTemplates = new List<Dictionary<string, string>>();
+                        foreach (XElement optionNode in node.Elements("Option"))
+                        {
+                            if (EvaluateConditional(currentTemplate, optionNode))
+                            {
+                                IEnumerable<Dictionary<string, string>> templateCopy = templates.Select(c => new Dictionary<string, string>(c));
+                                newTemplates.AddRange(ParseConfigSettings(templateCopy.ToArray(), optionNode));
+                            }
+                        }
+                        if (newTemplates.Count > 0)
+                        {
+                            return newTemplates.ToArray();
+                        }
+                    }
+                    break;
+
+                case "Import":
+                    if (EvaluateConditional(currentTemplate, node))
+                    {
+                        foreach (XAttribute attr in node.Attributes("ConfigFile"))
+                        {
+                            string file = ResolveProperties(currentTemplate, attr.Value).Trim();
+                            if (!Path.IsPathRooted(file))
+                            {
+                                file = Path.Combine(TestConfiguration.BaseDir, file);
+                            }
+                            templates = ParseConfigFile(file, templates);
+                        }
+                    }
+                    break;
+
+                default:
+                    foreach (Dictionary<string, string> config in templates)
+                    {
+                        // This checks the condition on an individual config value
+                        if (EvaluateConditional(config, node))
+                        {
+                            string resolveNodeValue = ResolveProperties(config, node.Value);
+                            config[node.Name.LocalName] = resolveNodeValue;
+                        }
+                    }
+                    break;
+            }
+            return templates;
+        }
+
+        bool EvaluateConditional(Dictionary<string, string> config, XElement node)
+        {
+            foreach (XAttribute attr in node.Attributes("Condition"))
+            {
+                string conditionText = attr.Value;
+
+                // Check if Exists('<directory or file>')
+                const string existsKeyword = "Exists('";
+                int existsStartIndex = conditionText.IndexOf(existsKeyword);
+                if (existsStartIndex != -1)
+                {
+                    bool not = (existsStartIndex > 0) && (conditionText[existsStartIndex - 1] == '!');
+
+                    existsStartIndex += existsKeyword.Length;
+                    int existsEndIndex = conditionText.IndexOf("')", existsStartIndex);
+                    Assert.NotEqual(-1, existsEndIndex);
+
+                    string path = conditionText.Substring(existsStartIndex, existsEndIndex - existsStartIndex);
+                    path = Path.GetFullPath(ResolveProperties(config, path));
+                    bool exists = Directory.Exists(path) || File.Exists(path);
+                    return not ? !exists : exists;
+                }
+                else
+                {
+                    // Check if equals and not equals
+                    string[] parts = conditionText.Split("==");
+                    bool equal;
+
+                    if (parts.Length == 2)
+                    {
+                        equal = true;
+                    }
+                    else
+                    {
+                        parts = conditionText.Split("!=");
+                        Assert.Equal(2, parts.Length);
+                        equal = false;
+                    }
+                    // Resolve any config values in the condition
+                    string leftValue = ResolveProperties(config, parts[0]).Trim();
+                    string rightValue = ResolveProperties(config, parts[1]).Trim();
+
+                    // Now do the simple string comparison of the left/right sides of the condition
+                    return equal ? leftValue == rightValue : leftValue != rightValue;
+                }
+            }
+            return true;
+        }
+
+        private string ResolveProperties(Dictionary<string, string> config, string rawNodeValue)
+        {
+            StringBuilder resolvedValue = new StringBuilder();
+            for(int i = 0; i < rawNodeValue.Length; )
+            {
+                int propStartIndex = rawNodeValue.IndexOf("$(", i);
+                if (propStartIndex == -1)
+                {
+                    if (i != rawNodeValue.Length)
+                    {
+                        resolvedValue.Append(rawNodeValue.Substring(i));
+                    }
+                    break;
+                }
+                else
+                {
+                    int propEndIndex = rawNodeValue.IndexOf(")", propStartIndex+1);
+                    Assert.NotEqual(-1, propEndIndex);
+                    if (propStartIndex != i)
+                    {
+                        resolvedValue.Append(rawNodeValue.Substring(i, propStartIndex - i));
+                    }
+                    // Now resolve the property name from the config dictionary
+                    string propertyName = rawNodeValue.Substring(propStartIndex + 2, propEndIndex - propStartIndex - 2);
+                    resolvedValue.Append(config.GetValueOrDefault(propertyName, ""));
+                    i = propEndIndex + 1;
+                }
+            }
+
+            return resolvedValue.ToString();
+        }
+
+        public void Dispose()
+        {
+        }
+    }
+
+    /// <summary>
+    /// Represents the current test configuration
+    /// </summary>
+    public class TestConfiguration
+    {
+        const string DebugTypeKey = "DebugType";
+        const string DebuggeeBuildRootKey = "DebuggeeBuildRoot";
+
+        internal static readonly string BaseDir = Path.GetFullPath(".");
+
+        private Dictionary<string, string> _settings;
+
+        public TestConfiguration()
+        {
+            _settings = new Dictionary<string, string>();
+        }
+
+        public TestConfiguration(Dictionary<string, string> initialSettings)
+        {
+            _settings = new Dictionary<string, string>(initialSettings);
+        }
+
+        public IReadOnlyDictionary<string, string> AllSettings
+        {
+            get { return _settings; }
+        }
+
+        public TestConfiguration CloneWithNewDebugType(string pdbType)
+        {
+            Debug.Assert(!string.IsNullOrWhiteSpace(pdbType));
+
+            var currentSettings = new Dictionary<string, string>(_settings);
+
+            // Set or replace if the pdb debug type
+            currentSettings[DebugTypeKey] = pdbType;
+
+            // The debuggee build root must exist. Append the pdb type to make it unique.
+            currentSettings[DebuggeeBuildRootKey] = Path.Combine(currentSettings[DebuggeeBuildRootKey], pdbType);
+
+            return new TestConfiguration(currentSettings);
+        }
+
+        /// <summary>
+        /// The target architecture (x64, x86, arm, arm64) to build and run. If the config
+        /// file doesn't have an TargetArchitecture property, then the current running
+        /// architecture is used.
+        /// </summary>
+        public string TargetArchitecture
+        {
+            get { return GetValue("TargetArchitecture").ToLowerInvariant(); }
+        }
+
+        /// <summary>
+        /// Built for "Debug" or "Release". Can be null.
+        /// </summary>
+        public string TargetConfiguration
+        {
+            get { return GetValue("TargetConfiguration"); }
+        }
+
+        /// <summary>
+        /// The product "projectk" (.NET Core) or "desktop".
+        /// </summary>
+        public string TestProduct
+        {
+            get { return GetValue("TestProduct").ToLowerInvariant(); }
+        }
+
+        /// <summary>
+        /// Returns true if running on .NET Core (based on TestProduct).
+        /// </summary>
+        public bool IsNETCore
+        {
+            get { return TestProduct.Equals("projectk"); }
+        }
+
+        /// <summary>
+        /// Returns true if running on desktop framework (based on TestProduct).
+        /// </summary>
+        public bool IsDesktop
+        {
+            get { return TestProduct.Equals("desktop"); }
+        }
+
+        /// <summary>
+        /// The test runner script directory 
+        /// </summary>
+        public string ScriptRootDir
+        {
+            get { return MakeCanonicalPath(GetValue("ScriptRootDir")); }
+        }
+
+        /// <summary>
+        /// Working temporary directory.
+        /// </summary>
+        public string WorkingDir
+        {
+            get { return MakeCanonicalPath(GetValue("WorkingDir")); }
+        }
+
+        /// <summary>
+        /// The host program to run a .NET Core or null for desktop/no host.
+        /// </summary>
+        public string HostExe
+        {
+            get { return MakeCanonicalExePath(GetValue("HostExe")); }
+        }
+
+        /// <summary>
+        /// Arguments to the HostExe.
+        /// </summary>
+        public string HostArgs
+        {
+            get { return GetValue("HostArgs"); }
+        }
+
+        /// <summary>
+        /// Environment variables to pass to the target process (via the ProcessRunner).
+        /// </summary>
+        public string HostEnvVars
+        {
+            get { return GetValue("HostEnvVars"); }
+        }
+
+        /// <summary>
+        /// Add the host environment variables to the process runner.
+        /// </summary>
+        /// <param name="runner">process runner instance</param>
+        public void AddHostEnvVars(ProcessRunner runner)
+        {
+            if (HostEnvVars != null)
+            {
+                string[] vars = HostEnvVars.Split(';');
+                foreach (string var in vars)
+                {
+                    if (string.IsNullOrEmpty(var))
+                    {
+                        continue;
+                    }
+                    string[] parts = var.Split('=');
+                    runner = runner.WithEnvironmentVariable(parts[0], parts[1]);
+                }
+            }
+        }
+
+        /// <summary>
+        /// The directory to the runtime (coreclr.dll, etc.) symbols
+        /// </summary>
+        public string RuntimeSymbolsPath
+        {
+            get { return MakeCanonicalPath(GetValue("RuntimeSymbolsPath")); }
+        }
+
+        /// <summary>
+        /// How the debuggees are built: "prebuilt" or "cli" (builds the debuggee during the test run with build and cli configuration).
+        /// </summary>
+        public string DebuggeeBuildProcess
+        {
+            get { return GetValue("DebuggeeBuildProcess")?.ToLowerInvariant(); }
+        }
+
+        /// <summary>
+        /// Debuggee sources and template project file will be retrieved from here: <DebuggeeSourceRoot>/<DebuggeeName>/[<DebuggeeName>]
+        /// </summary>
+        public string DebuggeeSourceRoot
+        {
+            get { return MakeCanonicalPath(GetValue("DebuggeeSourceRoot")); }
+        }
+
+        /// <summary>
+        /// Debuggee final sources/project file/binary outputs will be placed here: <DebuggeeBuildRoot>/<DebuggeeName>/
+        /// </summary>
+        public string DebuggeeBuildRoot
+        {
+            get { return MakeCanonicalPath(GetValue(DebuggeeBuildRootKey)); }
+        }
+
+        /// <summary>
+        /// Debuggee native binary dependencies will be retrieved from here.
+        /// </summary>
+        public string DebuggeeNativeLibRoot
+        {
+            get { return MakeCanonicalPath(GetValue("DebuggeeNativeLibRoot")); }
+        }
+
+        /// <summary>
+        /// The version of the Microsoft.NETCore.App package to reference when building the debuggee.
+        /// </summary>
+        public string BuildProjectMicrosoftNetCoreAppVersion
+        {
+            get { return GetValue("BuildProjectMicrosoftNetCoreAppVersion"); }
+        }
+
+        /// <summary>
+        /// The framework type/version used to build the debuggee like "netcoreapp2.0" or "netstandard1.0".
+        /// </summary>
+        public string BuildProjectFramework
+        {
+            get { return GetValue("BuildProjectFramework"); }
+        }
+
+        /// <summary>
+        /// Optional runtime identifier (RID) like "linux-x64" or "win-x86". If set, causes the debuggee to 
+        /// be built a as "standalone" dotnet cli project where the runtime is copied to the debuggee build 
+        /// root.
+        /// </summary>
+        public string BuildProjectRuntime
+        {
+            get { return GetValue("BuildProjectRuntime"); }
+        }
+
+        /// <summary>
+        /// The version of the Microsoft.NETCore.App package to reference when running the debuggee (i.e. 
+        /// using the dotnet cli --fx-version option).
+        /// </summary>
+        public string RuntimeFrameworkVersion 
+        {
+            get { return GetValue("RuntimeFrameworkVersion"); }
+        }
+
+        /// <summary>
+        /// The major portion of the runtime framework version
+        /// </summary>
+        public int RuntimeFrameworkVersionMajor
+        {
+            get {
+                string version = RuntimeFrameworkVersion;
+                if (version != null) {
+                    string[] parts = version.Split('.');
+                    if (parts.Length > 0) {
+                        if (int.TryParse(parts[0], out int major)) {
+                            return major;
+                        }
+                    }
+                }
+                throw new SkipTestException("RuntimeFrameworkVersion (major) is not valid");
+            }
+        }
+
+        /// <summary>
+        /// The type of PDB: "full" (Windows PDB) or "portable".
+        /// </summary>
+        public string DebugType
+        {
+            get { return GetValue(DebugTypeKey); }
+        }
+
+        /// <summary>
+        /// Either the local path to the dotnet cli to build or the URL of the runtime to download and install.
+        /// </summary>
+        public string CliPath
+        {
+            get { return MakeCanonicalPath(GetValue("CliPath")); }
+        }
+
+        /// <summary>
+        /// The local path to put the downloaded and decompressed runtime.
+        /// </summary>
+        public string CliCacheRoot
+        {
+            get { return MakeCanonicalPath(GetValue("CliCacheRoot")); }
+        }
+
+        /// <summary>
+        /// The version (i.e. 2.0.0) of the dotnet cli to use.
+        /// </summary>
+        public string CliVersion
+        {
+            get { return GetValue("CliVersion"); }
+        }
+
+        /// <summary>
+        /// The directory to cache the nuget packages on restore
+        /// </summary>
+        public string NuGetPackageCacheDir
+        {
+            get { return MakeCanonicalPath(GetValue("NuGetPackageCacheDir")); }
+        }
+
+        /// <summary>
+        /// The nuget package feeds separated by semicolons.
+        /// </summary>
+        public string NuGetPackageFeeds
+        {
+            get { return GetValue("NuGetPackageFeeds"); }
+        }
+
+        /// <summary>
+        /// If true, log the test output, etc. to the console.
+        /// </summary>
+        public bool LogToConsole
+        {
+            get { return bool.TryParse(GetValue("LogToConsole"), out bool b) && b; }
+        }
+
+        /// <summary>
+        /// The directory to put the test logs.
+        /// </summary>
+        public string LogDirPath
+        {
+            get { return MakeCanonicalPath(GetValue("LogDir")); }
+        }
+
+        /// <summary>
+        /// The "ILLink.Tasks" package version to reference or null.
+        /// </summary>
+        public string LinkerPackageVersion
+        {
+            get { return GetValue("LinkerPackageVersion"); }
+        }
+
+        #region Runtime Features properties
+
+        /// <summary>
+        /// Returns true if the "createdump" facility exists.
+        /// </summary>
+        public bool CreateDumpExists
+        {
+            get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor > 1; }
+        }
+
+        /// <summary>
+        /// Returns true if a stack overflow causes dump to be generated with createdump. 3.x has now started to
+        /// create dumps on stack overflow.
+        /// </summary>
+        public bool StackOverflowCreatesDump
+        {
+            get { return IsNETCore && RuntimeFrameworkVersionMajor >= 3; }
+        }
+
+        /// <summary>
+        /// Returns true if a stack overflow causes a SIGSEGV exception instead of aborting.
+        /// </summary>
+        public bool StackOverflowSIGSEGV
+        {
+            get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor == 1; }
+        }
+
+        #endregion
+
+        /// <summary>
+        /// Returns the configuration value for the key or null.
+        /// </summary>
+        /// <param name="key">name of the configuration value</param>
+        /// <returns>configuration value or null</returns>
+        public string GetValue(string key)
+        {
+            // unlike dictionary it is OK to ask for non-existent keys
+            // if the key doesn't exist the result is null
+            _settings.TryGetValue(key, out string settingValue);
+            return settingValue;
+        }
+
+        public static string MakeCanonicalExePath(string maybeRelativePath)
+        {
+            if (string.IsNullOrWhiteSpace(maybeRelativePath))
+            {
+                return null;
+            }
+            string maybeRelativePathWithExtension = maybeRelativePath;
+            if (OS.Kind == OSKind.Windows && !maybeRelativePath.EndsWith(".exe"))
+            {
+                maybeRelativePathWithExtension = maybeRelativePath + ".exe";
+            }
+            return MakeCanonicalPath(maybeRelativePathWithExtension);
+        }
+
+        public static string MakeCanonicalPath(string maybeRelativePath)
+        {
+            return MakeCanonicalPath(BaseDir, maybeRelativePath);
+        }
+
+        public static string MakeCanonicalPath(string baseDir, string maybeRelativePath)
+        {
+            if (string.IsNullOrWhiteSpace(maybeRelativePath))
+            {
+                return null;
+            }
+            // we will assume any path referencing an http endpoint is canonical already
+            if(maybeRelativePath.StartsWith("http:") ||
+               maybeRelativePath.StartsWith("https:"))
+            {
+                return maybeRelativePath;
+            }
+            string path = Path.IsPathRooted(maybeRelativePath) ? maybeRelativePath : Path.Combine(baseDir, maybeRelativePath);
+            path = Path.GetFullPath(path);
+            return OS.Kind != OSKind.Windows ? path.Replace('\\', '/') : path;
+        }
+
+        public override string ToString()
+        {
+            return TestProduct + "." + DebuggeeBuildProcess;
+        }
+    }
+
+    /// <summary>
+    /// The OS running
+    /// </summary>
+    public enum OSKind
+    {
+        Windows,
+        Linux,
+        OSX,
+        Unknown,
+    }
+
+    /// <summary>
+    /// The OS specific configuration
+    /// </summary>
+    public static class OS
+    {
+        static OS()
+        {
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+            {
+                Kind = OSKind.Linux;
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                Kind = OSKind.OSX;
+            }
+            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Kind = OSKind.Windows;
+            }
+            else
+            {
+                // Default to Unknown
+                Kind = OSKind.Unknown;
+            }
+        }
+
+        /// <summary>
+        /// The OS the tests are running.
+        /// </summary>
+        public static OSKind Kind { get; private set; }
+
+        /// <summary>
+        /// The architecture the tests are running.  We are assuming that the test runner, the debugger and the debugger's target are all the same architecture.
+        /// </summary>
+        public static Architecture TargetArchitecture { get { return RuntimeInformation.ProcessArchitecture; } }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs b/src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs
new file mode 100644 (file)
index 0000000..6966668
--- /dev/null
@@ -0,0 +1,144 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public class TestOutputProcessLogger : IProcessLogger
+    {
+        string _timeFormat = "mm\\:ss\\.fff";
+        ITestOutputHelper _output;
+        StringBuilder[] _lineBuffers;
+
+        public TestOutputProcessLogger(ITestOutputHelper output)
+        {
+            _output = output;
+            _lineBuffers = new StringBuilder[(int)ProcessStream.MaxStreams];
+        }
+
+        public void ProcessStarted(ProcessRunner runner)
+        {
+            lock (this)
+            {
+                _output.WriteLine("Running Process: " + runner.ReplayCommand);
+                _output.WriteLine("Working Directory: " + runner.WorkingDirectory);
+                IEnumerable<KeyValuePair<string,string>> additionalEnvVars = 
+                    runner.EnvironmentVariables.Where(kv => Environment.GetEnvironmentVariable(kv.Key) != kv.Value);
+
+                if(additionalEnvVars.Any())
+                {
+                    _output.WriteLine("Additional Environment Variables: " +
+                        string.Join(", ", additionalEnvVars.Select(kv => kv.Key + "=" + kv.Value)));
+                }
+                _output.WriteLine("{");
+            }
+        }
+
+        public virtual void Write(ProcessRunner runner, string data, ProcessStream stream)
+        {
+            lock (this)
+            {
+                AppendToLineBuffer(runner, stream, data);
+            }
+        }
+
+        public virtual void WriteLine(ProcessRunner runner, string data, ProcessStream stream)
+        {
+            lock (this)
+            {
+                StringBuilder lineBuffer = AppendToLineBuffer(runner, stream, data);
+                //Ensure all output is written even if it isn't a full line before we log input
+                if (stream == ProcessStream.StandardIn)
+                {
+                    FlushOutput();
+                }
+                _output.WriteLine(lineBuffer.ToString());
+                _lineBuffers[(int)stream] = null;
+            }
+        }
+
+        public virtual void ProcessExited(ProcessRunner runner)
+        {
+            lock (this)
+            {
+                TimeSpan offset = runner.StartTime - DateTime.Now;
+                _output.WriteLine("}");
+                _output.WriteLine("Exit code: " + runner.ExitCode + " ( " + offset.ToString(_timeFormat) + " elapsed)");
+                _output.WriteLine("");
+            }
+        }
+
+        public void ProcessKilled(ProcessRunner runner, KillReason reason)
+        {
+            lock (this)
+            {
+                TimeSpan offset = runner.StartTime - DateTime.Now;
+                string reasonText = "";
+                if (reason == KillReason.TimedOut)
+                {
+                    reasonText = "Process timed out";
+                }
+                else if (reason == KillReason.Unknown)
+                {
+                    reasonText = "Kill() was called";
+                }
+                _output.WriteLine("    Killing process: " + offset.ToString(_timeFormat) + ": " + reasonText);
+            }
+        }
+
+        protected void FlushOutput()
+        {
+            if (_lineBuffers[(int)ProcessStream.StandardOut] != null)
+            {
+                _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardOut].ToString());
+                _lineBuffers[(int)ProcessStream.StandardOut] = null;
+            }
+            if (_lineBuffers[(int)ProcessStream.StandardError] != null)
+            {
+                _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardError].ToString());
+                _lineBuffers[(int)ProcessStream.StandardError] = null;
+            }
+        }
+
+        private StringBuilder AppendToLineBuffer(ProcessRunner runner, ProcessStream stream, string data)
+        {
+            StringBuilder lineBuffer = _lineBuffers[(int)stream];
+            if (lineBuffer == null)
+            {
+                TimeSpan offset = runner.StartTime - DateTime.Now;
+                lineBuffer = new StringBuilder();
+                lineBuffer.Append("    ");
+                if (stream == ProcessStream.StandardError)
+                {
+                    lineBuffer.Append("STDERROR: ");
+                }
+                else if (stream == ProcessStream.StandardIn)
+                {
+                    lineBuffer.Append("STDIN: ");
+                }
+                lineBuffer.Append(offset.ToString(_timeFormat));
+                lineBuffer.Append(": ");
+                _lineBuffers[(int)stream] = lineBuffer;
+            }
+
+            // xunit has a bug where a non-printable character isn't properly escaped when
+            // it is written into the xml results which ultimately results in 
+            // the xml being improperly truncated. For example MDbg has a test case that prints
+            // \0 and dotnet tools print \u001B to colorize their console output.
+            foreach(char c in data)
+            {
+                if(!char.IsControl(c))
+                {
+                    lineBuffer.Append(c);
+                }
+            }
+            return lineBuffer;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs b/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs
new file mode 100644 (file)
index 0000000..c12c532
--- /dev/null
@@ -0,0 +1,257 @@
+// 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 System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    public class TestRunner
+    {
+        /// <summary>
+        /// Run debuggee (without any debugger) and compare the console output to the regex specified.
+        /// </summary>
+        /// <param name="config">test config to use</param>
+        /// <param name="output">output helper</param>
+        /// <param name="testName">test case name</param>
+        /// <param name="debuggeeName">debuggee name (no path)</param>
+        /// <param name="outputRegex">regex to match on console (standard and error) output</param>
+        /// <returns></returns>
+        public static async Task<int> Run(TestConfiguration config, ITestOutputHelper output, string testName, string debuggeeName, string outputRegex)
+        {
+            OutputHelper outputHelper = null;
+            try
+            {
+                // Setup the logging from the options in the config file
+                outputHelper = ConfigureLogging(config, output, testName);
+
+                // Restore and build the debuggee. The debuggee name is lower cased because the 
+                // source directory name has been lowercased by the build system.
+                DebuggeeConfiguration debuggeeConfig = await DebuggeeCompiler.Execute(config, debuggeeName.ToLowerInvariant(), outputHelper);
+
+                outputHelper.WriteLine("Starting {0}", testName);
+                outputHelper.WriteLine("{");
+
+                // Get the full debuggee launch command line (includes the host if required)
+                string exePath = debuggeeConfig.BinaryExePath;
+                string arguments = debuggeeConfig.BinaryDirPath;
+                if (!string.IsNullOrWhiteSpace(config.HostExe))
+                {
+                    exePath = config.HostExe;
+                    arguments = Environment.ExpandEnvironmentVariables(string.Format("{0} {1} {2}", config.HostArgs, debuggeeConfig.BinaryExePath, debuggeeConfig.BinaryDirPath));
+                }
+
+                TestLogger testLogger = new TestLogger(outputHelper.IndentedOutput);
+                ProcessRunner processRunner = new ProcessRunner(exePath, arguments).
+                    WithLog(testLogger).
+                    WithTimeout(TimeSpan.FromMinutes(5));
+
+                processRunner.Start();
+
+                // Wait for the debuggee to finish before getting the debuggee output
+                int exitCode = await processRunner.WaitForExit();
+
+                string debuggeeStandardOutput = testLogger.GetStandardOutput();
+                string debuggeeStandardError = testLogger.GetStandardError();
+
+                // The debuggee output is all the stdout first and then all the stderr output last
+                string debuggeeOutput = debuggeeStandardOutput + debuggeeStandardError;
+                if (string.IsNullOrEmpty(debuggeeOutput))
+                {
+                    throw new Exception("No debuggee output");
+                }
+                // Remove any CR's in the match string because this assembly is built on Windows (with CRs) and
+                // ran on Linux/OS X (without CRs).
+                outputRegex = outputRegex.Replace("\r", "");
+
+                // Now match the debuggee output and regex match string
+                if (!new Regex(outputRegex, RegexOptions.Multiline).IsMatch(debuggeeOutput))
+                {
+                    throw new Exception(string.Format("\nDebuggee output:\n\n'{0}'\n\nDid not match the expression:\n\n'{1}'", debuggeeOutput, outputRegex));
+                }
+
+                return exitCode;
+            }
+            catch (Exception ex)
+            {
+                // Log the exception
+                outputHelper?.WriteLine(ex.ToString());
+                throw;
+            }
+            finally
+            {
+                outputHelper?.WriteLine("}");
+                outputHelper?.Dispose();
+            }
+        }
+
+        /// <summary>
+        /// Returns a test config for each PDB type supported by the product/platform.
+        /// </summary>
+        /// <param name="config">starting config</param>
+        /// <returns>new configs for each supported PDB type</returns>
+        public static IEnumerable<TestConfiguration> EnumeratePdbTypeConfigs(TestConfiguration config)
+        {
+            string[] pdbTypes = { "portable", "embedded" };
+
+            if (OS.Kind == OSKind.Windows)
+            {
+                if (config.IsNETCore)
+                {
+                    pdbTypes = new string[] { "portable", "full", "embedded" };
+                }
+                else
+                {
+                    // Don't change the config on the desktop/projectn projects
+                    pdbTypes = new string[] { "" };
+                }
+            }
+
+            foreach (string pdbType in pdbTypes)
+            {
+                if (string.IsNullOrWhiteSpace(pdbType))
+                {
+                    yield return config;
+                }
+                else
+                {
+                    yield return config.CloneWithNewDebugType(pdbType);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns an output helper for the specified config.
+        /// </summary>
+        /// <param name="config">test config</param>
+        /// <param name="output">starting output helper</param>
+        /// <param name="testName">test case name</param>
+        /// <returns>new output helper</returns>
+        public static TestRunner.OutputHelper ConfigureLogging(TestConfiguration config, ITestOutputHelper output, string testName)
+        {
+            FileTestOutputHelper fileLogger = null;
+            ConsoleTestOutputHelper consoleLogger = null;
+            if (!string.IsNullOrEmpty(config.LogDirPath))
+            {
+                string logFileName = testName + "." + config.ToString() + ".log";
+                string logPath = Path.Combine(config.LogDirPath, logFileName);
+                fileLogger = new FileTestOutputHelper(logPath, FileMode.Append);
+            }
+            if (config.LogToConsole)
+            {
+                consoleLogger = new ConsoleTestOutputHelper();
+            }
+            return new TestRunner.OutputHelper(output, fileLogger, consoleLogger);
+        }
+
+        public class OutputHelper : ITestOutputHelper, IDisposable
+        {
+            readonly ITestOutputHelper _output;
+            readonly FileTestOutputHelper _fileLogger;
+            readonly ConsoleTestOutputHelper _consoleLogger;
+
+            public readonly ITestOutputHelper IndentedOutput;
+
+            public OutputHelper(ITestOutputHelper output, FileTestOutputHelper fileLogger, ConsoleTestOutputHelper consoleLogger)
+            {
+                _output = output;
+                _fileLogger = fileLogger;
+                _consoleLogger = consoleLogger;
+                IndentedOutput = new IndentedTestOutputHelper(this);
+            }
+
+            public void WriteLine(string message)
+            {
+                _output.WriteLine(message);
+                _fileLogger?.WriteLine(message);
+                _consoleLogger?.WriteLine(message);
+            }
+
+            public void WriteLine(string format, params object[] args)
+            {
+                _output.WriteLine(format, args);
+                _fileLogger?.WriteLine(format, args);
+                _consoleLogger?.WriteLine(format, args);
+            }
+
+            public void Dispose()
+            {
+                _fileLogger?.Dispose();
+            }
+        }
+
+        public class TestLogger : TestOutputProcessLogger
+        {
+            readonly StringBuilder _standardOutput;
+            readonly StringBuilder _standardError;
+
+            public TestLogger(ITestOutputHelper output)
+                : base(output)
+            {
+                lock (this)
+                {
+                    _standardOutput = new StringBuilder();
+                    _standardError = new StringBuilder();
+                }
+            }
+
+            public string GetStandardOutput()
+            {
+                lock (this)
+                {
+                    return _standardOutput.ToString();
+                }
+            }
+
+            public string GetStandardError()
+            {
+                lock (this)
+                {
+                    return _standardError.ToString();
+                }
+            }
+
+            public override void Write(ProcessRunner runner, string data, ProcessStream stream)
+            {
+                lock (this)
+                {
+                    base.Write(runner, data, stream);
+                    switch (stream)
+                    {
+                        case ProcessStream.StandardOut:
+                            _standardOutput.Append(data);
+                            break;
+
+                        case ProcessStream.StandardError:
+                            _standardError.Append(data);
+                            break;
+                    }
+                }
+            }
+
+            public override void WriteLine(ProcessRunner runner, string data, ProcessStream stream)
+            {
+                lock (this)
+                {
+                    base.WriteLine(runner, data, stream);
+                    switch (stream)
+                    {
+                        case ProcessStream.StandardOut:
+                            _standardOutput.AppendLine(data);
+                            break;
+
+                        case ProcessStream.StandardError:
+                            _standardError.AppendLine(data);
+                            break;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/TestStep.cs
new file mode 100644 (file)
index 0000000..e4c12bf
--- /dev/null
@@ -0,0 +1,636 @@
+// 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 System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Xunit.Abstractions;
+
+namespace Microsoft.Diagnostics.TestHelpers
+{
+    /// <summary>
+    /// An incremental atomic unit of work in the process of running a test. A test
+    /// can consist of multiple processes running across different machines at
+    /// different times. The TestStep supports:
+    /// 1) coordination between test processes to ensure each step runs only once
+    /// 2) disk based persistence so that later steps in different processes can
+    ///    reload the state of earlier steps
+    /// 3) Pretty printing logs
+    /// 4) TODO: Dependency analysis to determine if the cached output of a previous step
+    ///    execution is still valid
+    /// </summary>
+    public class TestStep
+    {
+        string _logFilePath;
+        string _stateFilePath;
+        TimeSpan _timeout;
+
+        public TestStep(string logFilePath, string friendlyName)
+        {
+            _logFilePath = logFilePath;
+            _stateFilePath = Path.ChangeExtension(_logFilePath, "state.txt");
+            _timeout = TimeSpan.FromMinutes(20);
+            FriendlyName = friendlyName;
+        }
+
+        public string FriendlyName { get; private set; }
+
+        async public Task Execute(ITestOutputHelper output)
+        {
+            // if this step is in progress on another thread, wait for it
+            TestStepState stepState = await AcquireStepStateLock(output);
+
+            //if this thread wins the race we do the work on this thread, otherwise
+            //we log the winner's saved output
+            if (stepState.RunState != TestStepRunState.InProgress)
+            {
+                LogHeader(stepState, true, output);
+                LogPreviousResults(stepState, output);
+                LogFooter(stepState, output);
+                ThrowExceptionIfFaulted(stepState);
+            }
+            else
+            {
+                await UncachedExecute(stepState, output);
+            }
+        }
+
+        protected virtual Task DoWork(ITestOutputHelper output)
+        {
+            output.WriteLine("Overload the default DoWork implementation in order to run useful work");
+            return Task.Delay(0);
+        }
+
+        private async Task UncachedExecute(TestStepState stepState, ITestOutputHelper output)
+        {
+            using (FileTestOutputHelper stepLog = new FileTestOutputHelper(_logFilePath))
+            {
+                try
+                {
+                    LogHeader(stepState, false, output);
+                    MultiplexTestOutputHelper mux = new MultiplexTestOutputHelper(new IndentedTestOutputHelper(output), stepLog);
+                    await DoWork(mux);
+                    stepState = stepState.Complete();
+                }
+                catch (Exception e)
+                {
+                    stepState = stepState.Fault(e.Message, e.StackTrace);
+                }
+                finally
+                {
+                    LogFooter(stepState, output);
+                    await WriteFinalStepState(stepState, output);
+                    ThrowExceptionIfFaulted(stepState);
+                }
+            }
+        }
+
+        private bool TryWriteInitialStepState(TestStepState state, ITestOutputHelper output)
+        {
+            // To ensure the file is atomically updated we write the contents to a temporary
+            // file, then move it to the final location
+            try
+            {
+                string tempPath = Path.GetTempFileName();
+                try
+                {
+                    File.WriteAllText(tempPath, state.SerializeInitialState());
+                    Directory.CreateDirectory(Path.GetDirectoryName(_stateFilePath));
+                    File.Move(tempPath, _stateFilePath);
+                    return true;
+                }
+                finally
+                {
+                    File.Delete(tempPath);
+                }
+                
+            }
+            catch (IOException ex)
+            {
+                output.WriteLine("Exception writing state file {0} {1}", _stateFilePath, ex.ToString());
+                return false;
+            }
+        }
+
+        private bool TryOpenExistingStepStateFile(out TestStepState stepState, ITestOutputHelper output)
+        {
+            stepState = null;
+            try
+            {
+                if (!Directory.Exists(Path.GetDirectoryName(_stateFilePath)))
+                {
+                    return false;
+                }
+                bool result = TestStepState.TryParse(File.ReadAllText(_stateFilePath), out stepState);
+                if (!result)
+                {
+                    output.WriteLine("TryParse failed on opening existing state file {0}", _stateFilePath);
+                }
+                return result;
+            }
+            catch (IOException ex)
+            {
+                output.WriteLine("Exception opening existing state file {0} {1}", _stateFilePath, ex.ToString());
+                return false;
+            }
+        }
+
+        async private Task WriteFinalStepState(TestStepState stepState, ITestOutputHelper output)
+        {
+            const int NumberOfRetries = 5;
+            FileStream stepStateStream = null;
+
+            // Retry few times because the state file may be open temporarily by another thread or process.
+            for (int retries = 0; retries < NumberOfRetries; retries++)
+            {
+                try
+                {
+                    stepStateStream = File.Open(_stateFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
+                    break;
+                }
+                catch (IOException ex)
+                {
+                    output.WriteLine("WriteFinalStepState exception {0} retry #{1}", ex.ToString(), retries);
+                    if (retries >= (NumberOfRetries - 1))
+                    {
+                        throw;
+                    }
+                }
+            }
+
+            using (stepStateStream)
+            {
+                stepStateStream.Seek(0, SeekOrigin.End);
+                StreamWriter writer = new StreamWriter(stepStateStream);
+                await writer.WriteAsync(Environment.NewLine + stepState.SerializeFinalState());
+                await writer.FlushAsync();
+            }
+        }
+
+        private void LogHeader(TestStepState stepState, bool cached, ITestOutputHelper output)
+        {
+            string cachedText = cached ? " (CACHED)" : "";
+            output.WriteLine("[" + stepState.StartTime + "] " + FriendlyName + cachedText);
+            output.WriteLine("Process: " + stepState.ProcessName + "(ID: 0x" + stepState.ProcessID.ToString("x") + ") on " + stepState.Machine);
+            output.WriteLine("{");
+        }
+
+        private void LogFooter(TestStepState stepState, ITestOutputHelper output)
+        {
+            output.WriteLine("}");
+            string elapsedTime = null;
+            if (stepState.RunState == TestStepRunState.InProgress)
+            {
+                output.WriteLine(FriendlyName + " Not Complete");
+                output.WriteLine(stepState.ErrorMessage);
+            }
+            else
+            {
+                elapsedTime = (stepState.CompleteTime.Value - stepState.StartTime).ToString("mm\\:ss\\.fff");
+            }
+            if (stepState.RunState == TestStepRunState.Complete)
+            {
+                output.WriteLine(FriendlyName + " Complete (" + elapsedTime + " elapsed)");
+            }
+            else if (stepState.RunState == TestStepRunState.Faulted)
+            {
+                output.WriteLine(FriendlyName + " Faulted (" + elapsedTime + " elapsed)");
+                output.WriteLine(stepState.ErrorMessage);
+                output.WriteLine(stepState.ErrorStackTrace);
+            }
+            output.WriteLine("");
+            output.WriteLine("");
+        }
+
+        private async Task<TestStepState> AcquireStepStateLock(ITestOutputHelper output)
+        {
+            TestStepState initialStepState = new TestStepState();
+            
+            bool stepStateFileExists = false;
+            while (true)
+            {
+                TestStepState openedStepState = null;
+                stepStateFileExists = File.Exists(_stateFilePath);
+                if (!stepStateFileExists && TryWriteInitialStepState(initialStepState, output))
+                {
+                    // this thread gets to do the work, persist the initial lock state
+                    return initialStepState;
+                }
+
+                if (stepStateFileExists && TryOpenExistingStepStateFile(out openedStepState, output))
+                {
+                    if (!ShouldReuseCachedStepState(openedStepState))
+                    {
+                        try
+                        {
+                            File.Delete(_stateFilePath);
+                            continue;
+                        }
+                        catch (IOException ex)
+                        {
+                            output.WriteLine("Exception deleting state file {0} {1}", _stateFilePath, ex.ToString());
+                        }
+                    }
+                    else if (openedStepState.RunState != TestStepRunState.InProgress)
+                    {
+                        // we can reuse the work and it is finished - stop waiting and return it
+                        return openedStepState;
+                    }
+                }
+
+                // If we get here we are either:
+                // a) Waiting for some other thread (potentially in another process) to complete the work
+                // b) Waiting for a hopefully transient IO issue to resolve so that we can determine whether or not the work has already been claimed
+                //
+                // If we wait for too long in either case we will eventually timeout.
+                ThrowExceptionForIncompleteWorkIfNeeded(initialStepState, openedStepState, stepStateFileExists, output);
+                await Task.Delay(TimeSpan.FromSeconds(1));
+            }
+        }
+
+        private void ThrowExceptionForIncompleteWorkIfNeeded(TestStepState initialStepState, TestStepState openedStepState, bool stepStateFileExists, ITestOutputHelper output)
+        {
+            bool timeout = (DateTimeOffset.Now - initialStepState.StartTime > _timeout);
+            bool notFinishable = openedStepState != null &&
+                                 ShouldReuseCachedStepState(openedStepState) &&
+                                 openedStepState.RunState == TestStepRunState.InProgress &&
+                                 !IsOpenedStateChangeable(openedStepState);
+            if (timeout || notFinishable)
+            {
+                TestStepState currentState = openedStepState != null ? openedStepState : initialStepState;
+                LogHeader(currentState, true, output);
+                StringBuilder errorMessage = new StringBuilder();
+                if (timeout)
+                {
+                    errorMessage.Append("Timeout after " + _timeout + ". ");
+                }
+                if (!stepStateFileExists)
+                {
+                    errorMessage.Append("Unable to create file:" + Environment.NewLine +
+                        _stateFilePath);
+                }
+                else if (openedStepState == null)
+                {
+                    errorMessage.AppendLine("Unable to parse file:" + Environment.NewLine +
+                        _stateFilePath);
+                }
+                else
+                {
+                    // these error cases should have a valid previous log we can restore
+                    Debug.Assert(currentState == openedStepState);
+                    LogPreviousResults(currentState, output);
+
+                    errorMessage.AppendLine("This step was not marked complete in: " + Environment.NewLine +
+                                            _stateFilePath);
+
+                    if (!IsPreviousMachineSame(openedStepState))
+                    {
+                        errorMessage.AppendLine("The current machine (" + Environment.MachineName + ") differs from the one which ran the step originally (" + currentState.Machine + ")." + Environment.NewLine +
+                                                "Perhaps the original process (ID: 0x" + currentState.ProcessID.ToString("x") + ") executing the work exited unexpectedly or the file was" + Environment.NewLine +
+                                                "copied to this machine before the work was complete?");
+                    }
+                    else if (IsPreviousMachineSame(openedStepState) && !IsPreviousProcessRunning(openedStepState))
+                    {
+                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " the process executing this step (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
+                                                "is no longer running. Perhaps it was killed or exited unexpectedly?");
+                    }
+                    else if (openedStepState.ProcessID != Process.GetCurrentProcess().Id)
+                    {
+                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " the process executing this step (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
+                                                "is still running. The process may be hung or running more slowly than expected?");
+                    }
+                    else
+                    {
+                        errorMessage.AppendLine("As of " + DateTimeOffset.Now + " this step should still be running on some other thread in this process (ID: 0x" + currentState.ProcessID.ToString("x") + ")" + Environment.NewLine +
+                                                "Perhaps the work has deadlocked or is running more slowly than expected?");
+                    }
+
+                    string reuseMessage = GetReuseStepStateReason(openedStepState);
+                    if (reuseMessage == null)
+                    {
+                        reuseMessage = "Deleting the file to retry the test step was attempted automatically, but failed.";
+                    }
+                    else
+                    {
+                        reuseMessage = "Deleting the file to retry the test step was not attempted automatically because " + reuseMessage + ".";
+                    }
+                    errorMessage.Append(reuseMessage);
+                }
+                currentState = currentState.Incomplete(errorMessage.ToString());
+                LogFooter(currentState, output);
+                if (timeout)
+                {
+                    throw new TestStepException("Timeout waiting for " + FriendlyName + " step to complete." + Environment.NewLine + errorMessage.ToString());
+                }
+                else
+                {
+                    throw new TestStepException(FriendlyName + " step can not be completed." + Environment.NewLine + errorMessage.ToString());
+                }
+            }
+        }
+
+        private static bool ShouldReuseCachedStepState(TestStepState openedStepState)
+        {
+            return (GetReuseStepStateReason(openedStepState) != null);
+        }
+
+        private static string GetReuseStepStateReason(TestStepState openedStepState)
+        {
+            //This heuristic may need to change, in some cases it is probably too eager to
+            //reuse past results when we wanted to retest something. 
+
+            if (openedStepState.RunState == TestStepRunState.Complete)
+            {
+                return "succesful steps are always reused";
+            }
+            else if(!IsPreviousMachineSame(openedStepState))
+            {
+                return "steps on run on other machines are always reused, regardless of success";
+            }
+            else if(IsPreviousProcessRunning(openedStepState))
+            {
+                return "steps run in currently executing processes are always reused, regardless of success";
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        private static bool IsPreviousMachineSame(TestStepState openedStepState)
+        {
+            return Environment.MachineName == openedStepState.Machine;
+        }
+
+        private static bool IsPreviousProcessRunning(TestStepState openedStepState)
+        {
+            Debug.Assert(IsPreviousMachineSame(openedStepState));
+            return (Process.GetProcesses().Any(p => p.Id == openedStepState.ProcessID && p.ProcessName == openedStepState.ProcessName));
+        }
+
+        private static bool IsOpenedStateChangeable(TestStepState openedStepState)
+        {
+            return (openedStepState.RunState == TestStepRunState.InProgress && 
+                    IsPreviousMachineSame(openedStepState) &&
+                    IsPreviousProcessRunning(openedStepState));
+        }
+
+        private void LogPreviousResults(TestStepState cachedTaskState, ITestOutputHelper output)
+        {
+            ITestOutputHelper indentedOutput = new IndentedTestOutputHelper(output);
+            try
+            {
+                string[] lines = File.ReadAllLines(_logFilePath);
+                foreach (string line in lines)
+                {
+                    indentedOutput.WriteLine(line);
+                }
+            }
+            catch (IOException e)
+            {
+                string errorMessage = "Error accessing task log file: " + _logFilePath + Environment.NewLine +
+                                      e.GetType().FullName + ": " + e.Message;
+                indentedOutput.WriteLine(errorMessage);
+            }
+        }
+
+        private void ThrowExceptionIfFaulted(TestStepState cachedStepState)
+        {
+            if(cachedStepState.RunState == TestStepRunState.Faulted)
+            {
+                throw new TestStepException(FriendlyName, cachedStepState.ErrorMessage, cachedStepState.ErrorStackTrace);
+            }
+        }
+
+        enum TestStepRunState
+        {
+            InProgress,
+            Complete,
+            Faulted
+        }
+
+        class TestStepState
+        {
+            public TestStepState()
+            {
+                RunState = TestStepRunState.InProgress;
+                Machine = Environment.MachineName;
+                ProcessID = Process.GetCurrentProcess().Id;
+                ProcessName = Process.GetCurrentProcess().ProcessName;
+                StartTime = DateTimeOffset.Now;
+            }
+            public TestStepState(TestStepRunState runState,
+                                 string machine,
+                                 int pid,
+                                 string processName,
+                                 DateTimeOffset startTime,
+                                 DateTimeOffset? completeTime,
+                                 string errorMessage,
+                                 string errorStackTrace)
+            {
+                RunState = runState;
+                Machine = machine;
+                ProcessID = pid;
+                ProcessName = processName;
+                StartTime = startTime;
+                CompleteTime = completeTime;
+                ErrorMessage = errorMessage;
+                ErrorStackTrace = errorStackTrace;
+            }
+            public TestStepRunState RunState { get; private set; }
+            public string Machine { get; private set; }
+            public int ProcessID { get; private set; }
+            public string ProcessName { get; private set; }
+            public string ErrorMessage { get; private set; }
+            public string ErrorStackTrace { get; private set; }
+            public DateTimeOffset StartTime { get; private set; }
+            public DateTimeOffset? CompleteTime { get; private set; }
+
+            public TestStepState Incomplete(string errorMessage)
+            {
+                return WithFinalState(TestStepRunState.InProgress, null, errorMessage, null);
+            }
+
+            public TestStepState Fault(string errorMessage, string errorStackTrace)
+            {
+                return WithFinalState(TestStepRunState.Faulted, DateTimeOffset.Now, errorMessage, errorStackTrace);
+            }
+
+            public TestStepState Complete()
+            {
+                return WithFinalState(TestStepRunState.Complete, DateTimeOffset.Now, null, null);
+            }
+
+            TestStepState WithFinalState(TestStepRunState runState, DateTimeOffset? taskCompleteTime, string errorMessage, string errorStackTrace)
+            {
+                return new TestStepState(runState, Machine, ProcessID, ProcessName, StartTime, taskCompleteTime, errorMessage, errorStackTrace);
+            }
+
+            public string SerializeInitialState()
+            {
+                XElement initState = new XElement("InitialStepState",
+                    new XElement("Machine", Machine),
+                    new XElement("ProcessID", "0x" + ProcessID.ToString("x")),
+                    new XElement("ProcessName", ProcessName),
+                    new XElement("StartTime", StartTime)
+                    );
+                return initState.ToString();
+            }
+
+            public string SerializeFinalState()
+            {
+                XElement finalState = new XElement("FinalStepState",
+                    new XElement("RunState", RunState)
+                    );
+                if (CompleteTime != null)
+                {
+                    finalState.Add(new XElement("CompleteTime", CompleteTime.Value));
+                }
+                if (ErrorMessage != null)
+                {
+                    finalState.Add(new XElement("ErrorMessage", ErrorMessage));
+                }
+                if (ErrorStackTrace != null)
+                {
+                    finalState.Add(new XElement("ErrorStackTrace", ErrorStackTrace));
+                }
+                return finalState.ToString();
+            }
+
+            public static bool TryParse(string text, out TestStepState parsedState)
+            {
+                parsedState = null;
+                try
+                {
+                    // The XmlReader is not happy with two root nodes so we crudely split them.
+                    int indexOfInitialStepStateElementEnd = text.IndexOf("</InitialStepState>");
+                    if(indexOfInitialStepStateElementEnd == -1)
+                    {
+                        return false;
+                    }
+                    int splitIndex = indexOfInitialStepStateElementEnd + "</InitialStepState>".Length;
+                    string initialStepStateText = text.Substring(0, splitIndex);
+                    string finalStepStateText = text.Substring(splitIndex);
+
+                    XElement initialStepStateElement = XElement.Parse(initialStepStateText);
+                    if (initialStepStateElement == null || initialStepStateElement.Name != "InitialStepState")
+                    {
+                        return false;
+                    }
+                    XElement machineElement = initialStepStateElement.Element("Machine");
+                    if (machineElement == null || string.IsNullOrWhiteSpace(machineElement.Value))
+                    {
+                        return false;
+                    }
+                    string machine = machineElement.Value;
+                    XElement processIDElement = initialStepStateElement.Element("ProcessID");
+                    int processID;
+                    if (processIDElement == null ||
+                        !processIDElement.Value.StartsWith("0x"))
+                    {
+                        return false;
+                    }
+                    string processIdNumberText = processIDElement.Value.Substring("0x".Length);
+                    if (!int.TryParse(processIdNumberText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out processID))
+                    {
+                        return false;
+                    }
+                    string processName = null;
+                    XElement processNameElement = initialStepStateElement.Element("ProcessName");
+                    if (processNameElement != null)
+                    {
+                        processName = processNameElement.Value;
+                    }
+                    DateTimeOffset startTime;
+                    XElement startTimeElement = initialStepStateElement.Element("StartTime");
+                    if (startTimeElement == null || !DateTimeOffset.TryParse(startTimeElement.Value, out startTime))
+                    {
+                        return false;
+                    }
+                    parsedState = new TestStepState(TestStepRunState.InProgress, machine, processID, processName, startTime, null, null, null);
+                    TryParseFinalState(finalStepStateText, ref parsedState);
+                    return true;
+                }
+                catch (XmlException)
+                {
+                    return false;
+                }
+            }
+
+            private static void TryParseFinalState(string text, ref TestStepState taskState)
+            {
+                // If there are errors reading the final state portion of the stream we need to treat it
+                // as if the stream had terminated at the end of the InitialTaskState node.
+                // This covers a small window of time when appending the FinalTaskState node is in progress.
+                //
+                if(string.IsNullOrWhiteSpace(text))
+                {
+                    return;
+                }
+                try
+                {
+                    XElement finalTaskStateElement = XElement.Parse(text);
+                    if (finalTaskStateElement == null || finalTaskStateElement.Name != "FinalStepState")
+                    {
+                        return;
+                    }
+                    XElement runStateElement = finalTaskStateElement.Element("RunState");
+                    TestStepRunState runState;
+                    if (runStateElement == null || !Enum.TryParse<TestStepRunState>(runStateElement.Value, out runState))
+                    {
+                        return;
+                    }
+                    DateTimeOffset? completeTime = null;
+                    XElement completeTimeElement = finalTaskStateElement.Element("CompleteTime");
+                    if (completeTimeElement != null)
+                    {
+                        DateTimeOffset tempCompleteTime;
+                        if (!DateTimeOffset.TryParse(completeTimeElement.Value, out tempCompleteTime))
+                        {
+                            return;
+                        }
+                        else
+                        {
+                            completeTime = tempCompleteTime;
+                        }
+                    }
+                    XElement errorMessageElement = finalTaskStateElement.Element("ErrorMessage");
+                    string errorMessage = null;
+                    if (errorMessageElement != null)
+                    {
+                        errorMessage = errorMessageElement.Value;
+                    }
+                    XElement errorStackTraceElement = finalTaskStateElement.Element("ErrorStackTrace");
+                    string errorStackTrace = null;
+                    if (errorStackTraceElement != null)
+                    {
+                        errorStackTrace = errorStackTraceElement.Value;
+                    }
+
+                    taskState = taskState.WithFinalState(runState, completeTime, errorMessage, errorStackTrace);
+                }
+                catch (XmlException) { }
+            }
+        }
+    }
+
+    public class TestStepException : Exception
+    {
+        public TestStepException(string errorMessage) :
+            base(errorMessage)
+        { }
+
+        public TestStepException(string stepName, string errorMessage, string stackTrace) :
+            base("The " + stepName + " test step failed." + Environment.NewLine +
+                 "Original Error: " + errorMessage + Environment.NewLine +
+                 stackTrace)
+        { }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs
new file mode 100644 (file)
index 0000000..21c5dbf
--- /dev/null
@@ -0,0 +1,10 @@
+using System;
+
+namespace Xunit.Extensions
+{
+    public class SkipTestException : Exception
+    {
+        public SkipTestException(string reason)
+            : base(reason) { }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs
new file mode 100644 (file)
index 0000000..f3490ae
--- /dev/null
@@ -0,0 +1,8 @@
+using Xunit;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostics.TestHelpers")]
+    public class SkippableFactAttribute : FactAttribute { }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs
new file mode 100644 (file)
index 0000000..b249160
--- /dev/null
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer
+    {
+        readonly IMessageSink diagnosticMessageSink;
+
+        public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink)
+        {
+            this.diagnosticMessageSink = diagnosticMessageSink;
+        }
+
+        public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
+        {
+            yield return new SkippableFactTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(),  discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs
new file mode 100644 (file)
index 0000000..9a7fa30
--- /dev/null
@@ -0,0 +1,37 @@
+using System.Linq;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    public class SkippableFactMessageBus : IMessageBus
+    {
+        readonly IMessageBus innerBus;
+
+        public SkippableFactMessageBus(IMessageBus innerBus)
+        {
+            this.innerBus = innerBus;
+        }
+
+        public int DynamicallySkippedTestCount { get; private set; }
+
+        public void Dispose() { }
+
+        public bool QueueMessage(IMessageSinkMessage message)
+        {
+            var testFailed = message as ITestFailed;
+            if (testFailed != null)
+            {
+                var exceptionType = testFailed.ExceptionTypes.FirstOrDefault();
+                if (exceptionType == typeof(SkipTestException).FullName)
+                {
+                    DynamicallySkippedTestCount++;
+                    return innerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault()));
+                }
+            }
+
+            // Nothing we care about, send it on its way
+            return innerBus.QueueMessage(message);
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs
new file mode 100644 (file)
index 0000000..e031171
--- /dev/null
@@ -0,0 +1,34 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    public class SkippableFactTestCase : XunitTestCase
+    {
+        [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
+        public SkippableFactTestCase() { }
+
+        public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null)
+            : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { }
+
+        public override async Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink,
+                                                        IMessageBus messageBus,
+                                                        object[] constructorArguments,
+                                                        ExceptionAggregator aggregator,
+                                                        CancellationTokenSource cancellationTokenSource)
+        {
+            var skipMessageBus = new SkippableFactMessageBus(messageBus);
+            var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource);
+            if (skipMessageBus.DynamicallySkippedTestCount > 0)
+            {
+                result.Failed -= skipMessageBus.DynamicallySkippedTestCount;
+                result.Skipped += skipMessageBus.DynamicallySkippedTestCount;
+            }
+
+            return result;
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs
new file mode 100644 (file)
index 0000000..c582350
--- /dev/null
@@ -0,0 +1,7 @@
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostics.TestHelpers")]
+    public class SkippableTheoryAttribute : TheoryAttribute { }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs
new file mode 100644 (file)
index 0000000..40c4ab7
--- /dev/null
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    public class SkippableTheoryDiscoverer : IXunitTestCaseDiscoverer
+    {
+        readonly IMessageSink diagnosticMessageSink;
+        readonly TheoryDiscoverer theoryDiscoverer;
+
+        public SkippableTheoryDiscoverer(IMessageSink diagnosticMessageSink)
+        {
+            this.diagnosticMessageSink = diagnosticMessageSink;
+
+            theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink);
+        }
+
+        public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
+        {
+            var defaultMethodDisplay = discoveryOptions.MethodDisplayOrDefault();
+            var defaultMethodDisplayOptions = discoveryOptions.MethodDisplayOptionsOrDefault();
+
+            // Unlike fact discovery, the underlying algorithm for theories is complex, so we let the theory discoverer
+            // do its work, and do a little on-the-fly conversion into our own test cases.
+            return theoryDiscoverer.Discover(discoveryOptions, testMethod, factAttribute)
+                                   .Select(testCase => testCase is XunitTheoryTestCase
+                                                           ? (IXunitTestCase)new SkippableTheoryTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod)
+                                                           : new SkippableFactTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod, testCase.TestMethodArguments));
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs
new file mode 100644 (file)
index 0000000..46d2037
--- /dev/null
@@ -0,0 +1,35 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+    public class SkippableTheoryTestCase : XunitTheoryTestCase
+    {
+        [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
+        public SkippableTheoryTestCase() { }
+
+        public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod)
+            : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { }
+
+        public override async Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink,
+                                                        IMessageBus messageBus,
+                                                        object[] constructorArguments,
+                                                        ExceptionAggregator aggregator,
+                                                        CancellationTokenSource cancellationTokenSource)
+        {
+            // Duplicated code from SkippableFactTestCase. I'm sure we could find a way to de-dup with some thought.
+            var skipMessageBus = new SkippableFactMessageBus(messageBus);
+            var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource);
+            if (skipMessageBus.DynamicallySkippedTestCount > 0)
+            {
+                result.Failed -= skipMessageBus.DynamicallySkippedTestCount;
+                result.Skipped += skipMessageBus.DynamicallySkippedTestCount;
+            }
+
+            return result;
+        }
+    }
+}
diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt
new file mode 100644 (file)
index 0000000..39b2d65
--- /dev/null
@@ -0,0 +1,15 @@
+Source in this directory is derived source at https://github.com/xunit/samples.xunit. The repo provided the following license:
+
+Copyright 2014 Outercurve Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
index bf38f5910bf45ec3dec3628005b28f48e3b71577..828eaab8b7f894291376f07e7449022eb20e8304 100644 (file)
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </Content>
   </ItemGroup>
-  
-  <ItemGroup>
-    <ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Microsoft.Diagnostic.TestHelpers\Microsoft.Diagnostic.TestHelpers.csproj" />
-  </ItemGroup>
 
   <ItemGroup>
     <PackageReference Include="Microsoft.Win32.Primitives" Version="$(MicrosoftWin32PrimitivesVersion)" />
     <PackageReference Include="cdb-sos" Version="$(cdbsosversion)" Condition="'$(OS)' == 'Windows_NT'" />
   </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\Microsoft.Diagnostics.TestHelpers\Microsoft.Diagnostics.TestHelpers.csproj" />
+  </ItemGroup>
 </Project>
index 16f4392038b3c15ffc78ba72ef3291811d39fe7b..8db30a6c6c4a2c54bfc4bce68033cb79478d97a6 100644 (file)
@@ -2,7 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using Microsoft.Diagnostic.TestHelpers;
+using Microsoft.Diagnostics.TestHelpers;
 using System;
 using System.Collections.Generic;
 using System.IO;
index 839df9d59f55e1488d09cdb467e525094a8d3ee7..11a81b410a7f5fb94ffc7f982781ccf131c5ac24 100644 (file)
@@ -2,7 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using Microsoft.Diagnostic.TestHelpers;
+using Microsoft.Diagnostics.TestHelpers;
 using System;
 using System.Collections.Generic;
 using System.IO;