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
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
+++ /dev/null
-// 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();
- }
-
- }
-}
+++ /dev/null
-// 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);
- }
- }
- }
-}
-
-
+++ /dev/null
-// 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);
- }
-}
+++ /dev/null
-// 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);
- }
- }
-}
+++ /dev/null
-// 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
+++ /dev/null
-// 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);
- }
- }
-}
+++ /dev/null
-// 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; }
- }
-}
+++ /dev/null
-// 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);
- }
-}
+++ /dev/null
-// 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();
- }
- }
- }
-}
+++ /dev/null
-// 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
+++ /dev/null
-// 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);
- }
- }
-}
+++ /dev/null
-<!-- 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>
+++ /dev/null
-// 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
+++ /dev/null
-// 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
+++ /dev/null
-// 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);
- }
- }
- }
- }
-}
+++ /dev/null
-// 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; } }
- }
-}
+++ /dev/null
-// 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
+++ /dev/null
-// 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;
- }
- }
- }
- }
- }
-}
+++ /dev/null
-// 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)
- { }
- }
-}
+++ /dev/null
-using System;
-
-namespace Xunit.Extensions
-{
- public class SkipTestException : Exception
- {
- public SkipTestException(string reason)
- : base(reason) { }
- }
-}
+++ /dev/null
-using Xunit;
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
- [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostic.TestHelpers")]
- public class SkippableFactAttribute : FactAttribute { }
-}
+++ /dev/null
-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);
- }
- }
-}
+++ /dev/null
-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);
- }
- }
-}
+++ /dev/null
-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;
- }
- }
-}
+++ /dev/null
-using Xunit.Sdk;
-
-namespace Xunit.Extensions
-{
- [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostic.TestHelpers")]
- public class SkippableTheoryAttribute : TheoryAttribute { }
-}
+++ /dev/null
-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));
- }
- }
-}
+++ /dev/null
-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;
- }
- }
-}
+++ /dev/null
-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
--- /dev/null
+// 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();
+ }
+
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+ }
+}
+
+
--- /dev/null
+// 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);
+ }
+}
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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; }
+ }
+}
--- /dev/null
+// 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);
+ }
+}
--- /dev/null
+// 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();
+ }
+ }
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+<!-- 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>
--- /dev/null
+// 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
--- /dev/null
+// 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
--- /dev/null
+// 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// 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; } }
+ }
+}
--- /dev/null
+// 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
--- /dev/null
+// 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;
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+// 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)
+ { }
+ }
+}
--- /dev/null
+using System;
+
+namespace Xunit.Extensions
+{
+ public class SkipTestException : Exception
+ {
+ public SkipTestException(string reason)
+ : base(reason) { }
+ }
+}
--- /dev/null
+using Xunit;
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+ [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostics.TestHelpers")]
+ public class SkippableFactAttribute : FactAttribute { }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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;
+ }
+ }
+}
--- /dev/null
+using Xunit.Sdk;
+
+namespace Xunit.Extensions
+{
+ [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostics.TestHelpers")]
+ public class SkippableTheoryAttribute : TheoryAttribute { }
+}
--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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;
+ }
+ }
+}
--- /dev/null
+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
<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>
// 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;
// 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;