From: Mike McLaughlin Date: Thu, 27 Jun 2019 19:59:46 +0000 (-0700) Subject: Rename Microsoft.Diagnostics.TestHelpers (#366) X-Git-Tag: submit/tizen/20190813.035844~4^2^2~17 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=db4fc696c0164f141e32742f850ecdcd5f7f8b55;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Rename Microsoft.Diagnostics.TestHelpers (#366) --- diff --git a/diagnostics.sln b/diagnostics.sln index 442e06fb9..d561e7a35 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29019.234 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.NETCore", "src\SOS\SOS.NETCore\SOS.NETCore.csproj", "{20513BA2-A156-4A17-4C70-5AC2DBD4F833}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDebuggee", "src\SOS\lldbplugin.tests\TestDebuggee\TestDebuggee.csproj", "{6C43BE85-F8C3-4D76-8050-F25CE953A7FD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostic.TestHelpers", "src\Microsoft.Diagnostic.TestHelpers\Microsoft.Diagnostic.TestHelpers.csproj", "{730C1201-1848-4F1E-8C1F-6316FB886C35}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.TestHelpers", "src\Microsoft.Diagnostics.TestHelpers\Microsoft.Diagnostics.TestHelpers.csproj", "{730C1201-1848-4F1E-8C1F-6316FB886C35}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SOS", "SOS", "{41638A4C-0DAF-47ED-A774-ECBBAC0315D7}" EndProject @@ -43,7 +43,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-counters", "src\Tool EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Tools.RuntimeClient", "src\Microsoft.Diagnostics.Tools.RuntimeClient\Microsoft.Diagnostics.Tools.RuntimeClient.csproj", "{54C240C5-7932-4421-A5FB-75205DE0B824}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreateVersionFile", "eng\CreateVersionFile.csproj", "{54E3BCB7-6094-4B25-AC44-D4F914438F03}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateVersionFile", "eng\CreateVersionFile.csproj", "{54E3BCB7-6094-4B25-AC44-D4F914438F03}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs deleted file mode 100644 index e18cfb85b..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/AcquireDotNetTestStep.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// Acquires the CLI tools from a web endpoint, a local zip/tar.gz, or directly from a local path - /// - public class AcquireDotNetTestStep : TestStep - { - /// - /// Create a new AcquireDotNetTestStep - /// - /// - /// 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 - /// - /// 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 - /// - /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar - /// file. Otherwise this path is unused. - /// - /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the - /// archive. Otherwise this path is unused. - /// - /// 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. - /// - /// The path where an activity log for this test step should be written. - /// - /// - 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; - } - - /// - /// 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. - /// - public string RemoteDotNetPath { get; private set; } - - /// - /// 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. - /// - public string LocalDotNetZipPath { get; private set; } - - /// - /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar - /// file. Otherwise null. - /// - public string LocalDotNetTarPath { get; private set; } - - /// - /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the - /// archive. Otherwise null. - /// - public string LocalDotNetZipExpandDirPath { get; private set; } - - /// - /// The path to the dotnet binary when the test step is complete. - /// - public string LocalDotNetPath { get; private set; } - - /// - /// Returns true, if there any actual work to do (like downloading, unziping or untaring). - /// - public bool AnyWorkToDo { get { return RemoteDotNetPath != null || LocalDotNetZipPath != null; } } - - async protected override Task DoWork(ITestOutputHelper output) - { - if (RemoteDotNetPath != null) - { - await DownloadFile(RemoteDotNetPath, LocalDotNetZipPath, output); - } - if (LocalDotNetZipPath != null) - { - if (LocalDotNetZipPath.EndsWith(".zip")) - { - await Unzip(LocalDotNetZipPath, LocalDotNetZipExpandDirPath, output); - } - else if(LocalDotNetZipPath.EndsWith(".tar.gz")) - { - await UnGZip(LocalDotNetZipPath, LocalDotNetTarPath, output); - await Untar(LocalDotNetTarPath, LocalDotNetZipExpandDirPath, output); - } - else - { - output.WriteLine("Unsupported compression format: " + LocalDotNetZipPath); - throw new NotSupportedException("Unsupported compression format: " + LocalDotNetZipPath); - } - } - output.WriteLine("Dotnet path: " + LocalDotNetPath); - if (!File.Exists(LocalDotNetPath)) - { - throw new FileNotFoundException(LocalDotNetPath + " not found"); - } - } - - async static Task DownloadFile(string remotePath, string localPath, ITestOutputHelper output) - { - output.WriteLine("Downloading: " + remotePath + " -> " + localPath); - Directory.CreateDirectory(Path.GetDirectoryName(localPath)); - WebRequest request = HttpWebRequest.Create(remotePath); - WebResponse response = await request.GetResponseAsync(); - using (FileStream localZipStream = File.OpenWrite(localPath)) - { - // TODO: restore the CopyToAsync code after System.Net.Http.dll is - // updated to a newer version. The current old version has a bug - // where the copy never finished. - // await response.GetResponseStream().CopyToAsync(localZipStream); - byte[] buffer = new byte[16 * 1024]; - long bytesLeft = response.ContentLength; - - while (bytesLeft > 0) - { - int read = response.GetResponseStream().Read(buffer, 0, buffer.Length); - if (read == 0) - break; - localZipStream.Write(buffer, 0, read); - bytesLeft -= read; - } - output.WriteLine("Downloading finished"); - } - } - - async static Task UnGZip(string gzipPath, string expandedFilePath, ITestOutputHelper output) - { - output.WriteLine("Unziping: " + gzipPath + " -> " + expandedFilePath); - using (FileStream gzipStream = File.OpenRead(gzipPath)) - { - using (GZipStream expandedStream = new GZipStream(gzipStream, CompressionMode.Decompress)) - { - using (FileStream targetFileStream = File.OpenWrite(expandedFilePath)) - { - await expandedStream.CopyToAsync(targetFileStream); - } - } - } - } - - async static Task Unzip(string zipPath, string expandedDirPath, ITestOutputHelper output) - { - output.WriteLine("Unziping: " + zipPath + " -> " + expandedDirPath); - using (FileStream zipStream = File.OpenRead(zipPath)) - { - ZipArchive zip = new ZipArchive(zipStream); - foreach (ZipArchiveEntry entry in zip.Entries) - { - string extractedFilePath = Path.Combine(expandedDirPath, entry.FullName); - Directory.CreateDirectory(Path.GetDirectoryName(extractedFilePath)); - using (Stream zipFileStream = entry.Open()) - { - using (FileStream extractedFileStream = File.OpenWrite(extractedFilePath)) - { - await zipFileStream.CopyToAsync(extractedFileStream); - } - } - } - } - } - - async static Task Untar(string tarPath, string expandedDirPath, ITestOutputHelper output) - { - Directory.CreateDirectory(expandedDirPath); - string tarToolPath = null; - if (OS.Kind == OSKind.Linux) - { - tarToolPath = "/bin/tar"; - } - else if (OS.Kind == OSKind.OSX) - { - tarToolPath = "/usr/bin/tar"; - } - else - { - throw new NotSupportedException("Unknown where this OS stores the tar executable"); - } - - await new ProcessRunner(tarToolPath, "-xf " + tarPath). - WithWorkingDirectory(expandedDirPath). - WithLog(output). - WithExpectedExitCode(0). - Run(); - } - - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/AssertX.cs b/src/Microsoft.Diagnostic.TestHelpers/AssertX.cs deleted file mode 100644 index 2672ca9b7..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/AssertX.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public static partial class AssertX - { - public static void DirectoryExists(string dirDescriptiveName, string dirPath, ITestOutputHelper output) - { - if (!Directory.Exists(dirPath)) - { - string errorMessage = "Expected " + dirDescriptiveName + " to exist: " + dirPath; - output.WriteLine(errorMessage); - try - { - string parentDir = dirPath; - while (true) - { - if (Directory.Exists(Path.GetDirectoryName(parentDir))) - { - output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir)); - break; - } - if (Path.GetDirectoryName(parentDir) == parentDir) - { - output.WriteLine("Also unable to find any parent directory that exists"); - break; - } - parentDir = Path.GetDirectoryName(parentDir); - } - } - catch (Exception e) - { - output.WriteLine("Additional error while trying to diagnose missing directory:"); - output.WriteLine(e.GetType() + ": " + e.Message); - } - throw new DirectoryNotFoundException(errorMessage); - } - } - - public static void FileExists(string fileDescriptiveName, string filePath, ITestOutputHelper output) - { - if (!File.Exists(filePath)) - { - string errorMessage = "Expected " + fileDescriptiveName + " to exist: " + filePath; - output.WriteLine(errorMessage); - try - { - string parentDir = filePath; - while (true) - { - if (Directory.Exists(Path.GetDirectoryName(parentDir))) - { - output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir)); - break; - } - if (Path.GetDirectoryName(parentDir) == parentDir) - { - output.WriteLine("Also unable to find any parent directory that exists"); - break; - } - parentDir = Path.GetDirectoryName(parentDir); - } - } - catch (Exception e) - { - output.WriteLine("Additional error while trying to diagnose missing file:"); - output.WriteLine(e.GetType() + ": " + e.Message); - } - throw new FileNotFoundException(errorMessage); - } - } - } -} - - diff --git a/src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs deleted file mode 100644 index af2fa2101..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/BaseDebuggeeCompiler.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// This compiler acquires the CLI tools and uses them to build debuggees. - /// - /// - /// 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 - /// - public abstract class BaseDebuggeeCompiler : IDebuggeeCompiler - { - AcquireDotNetTestStep _acquireTask; - DotNetBuildDebuggeeTestStep _buildDebuggeeTask; - - /// - /// Creates a new BaseDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build debuggees via dotnet build. - /// - /// - /// 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 - /// - /// - /// 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 - /// - public BaseDebuggeeCompiler(TestConfiguration config, string debuggeeName) - { - _acquireTask = ConfigureAcquireDotNetTask(config); - _buildDebuggeeTask = ConfigureDotNetBuildDebuggeeTask(config, _acquireTask.LocalDotNetPath, config.CliVersion, debuggeeName); - } - - async public Task 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 GetNugetFeeds(TestConfiguration config) - { - Dictionary nugetFeeds = new Dictionary(); - 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 = 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: / - //DebuggeeNativeLibDir: / - //DotNetRootBuildDir: - //DebuggeeSolutionDir: / - //DebuggeeProjectDir: /[/] - //DebuggeeBinaryDir: /[/]/bin/Debug//[] - //DebuggeeBinaryDll: /[/]/bin/Debug//.dll - //DebuggeeBinaryExe: /[/]/bin/Debug//[]/.exe - //LogPath: /.txt - - // When the runtime directory is present it will have a native host exe in it that has been renamed to the debugee - // name. It also has a managed dll in it which functions as a managed exe when renamed. - // When the runtime directory is missing, the framework directory will have a managed dll in it that functions if it - // is renamed to an exe. I'm sure that renaming isn't the intended usage, but it works and produces less churn - // in our tests for the moment. - public abstract DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName); - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs deleted file mode 100644 index 51f619f45..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/CliDebuggeeCompiler.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.IO; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish. - /// - public class CliDebuggeeCompiler : BaseDebuggeeCompiler - { - /// - /// Creates a new CliDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish. - /// - /// LinkerPackageVersion If set, this version of the linker package will be used to link the debuggee during publish. - /// - /// - public CliDebuggeeCompiler(TestConfiguration config, string debuggeeName) : base(config, debuggeeName) {} - - private static Dictionary GetBuildProperties(TestConfiguration config, string runtimeIdentifier) - { - Dictionary buildProperties = new Dictionary(); - buildProperties.Add("RuntimeFrameworkVersion", config.BuildProjectMicrosoftNetCoreAppVersion); - buildProperties.Add("BuildProjectFramework", config.BuildProjectFramework); - if (runtimeIdentifier != null) - { - buildProperties.Add("RuntimeIdentifier", runtimeIdentifier); - } - string debugType = config.DebugType; - if (debugType == null) - { - // The default PDB type is portable - debugType = "portable"; - } - buildProperties.Add("DebugType", debugType); - return buildProperties; - } - - protected override string GetFramework(TestConfiguration config) - { - return config.BuildProjectFramework ?? "netcoreapp2.0"; - } - - protected override string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime) - { - string debuggeeBinaryDirPath = base.GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtime); - debuggeeBinaryDirPath = Path.Combine(debuggeeBinaryDirPath, "publish"); - return debuggeeBinaryDirPath; - } - - public override DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName) - { - string runtimeIdentifier = GetRuntime(config); - string framework = GetFramework(config); - string initialSourceDirPath = GetInitialSourceDirPath(config, debuggeeName); - string dotNetRootBuildDirPath = GetDotNetRootBuildDirPath(config); - string debuggeeSolutionDirPath = GetDebuggeeSolutionDirPath(dotNetRootBuildDirPath, debuggeeName); - string debuggeeProjectDirPath = GetDebuggeeProjectDirPath(debuggeeSolutionDirPath, initialSourceDirPath, debuggeeName); - string debuggeeBinaryDirPath = GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtimeIdentifier); - string debuggeeBinaryDllPath = config.IsNETCore ? GetDebuggeeBinaryDllPath(debuggeeBinaryDirPath, debuggeeName) : null; - string debuggeeBinaryExePath = config.IsDesktop ? GetDebuggeeBinaryExePath(debuggeeBinaryDirPath, debuggeeName) : null; - string logPath = GetLogPath(config, framework, runtimeIdentifier, debuggeeName); - return new CsprojBuildDebuggeeTestStep(dotNetPath, - initialSourceDirPath, - GetDebuggeeNativeLibDirPath(config, debuggeeName), - GetBuildProperties(config, runtimeIdentifier), - runtimeIdentifier, - config.LinkerPackageVersion, - debuggeeName, - debuggeeSolutionDirPath, - debuggeeProjectDirPath, - debuggeeBinaryDirPath, - debuggeeBinaryDllPath, - debuggeeBinaryExePath, - config.NuGetPackageCacheDir, - GetNugetFeeds(config), - logPath); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs deleted file mode 100644 index 38b5af98b..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/ConsoleTestOutputHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public class ConsoleTestOutputHelper : ITestOutputHelper - { - public void WriteLine(string message) - { - Console.WriteLine(message); - } - - public void WriteLine(string format, params object[] args) - { - Console.WriteLine(format, args); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs deleted file mode 100644 index 914189ee2..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/CsprojBuildDebuggeeTestStep.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using System.Xml.Linq; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// This test step builds debuggees using the dotnet tools with .csproj projects files. - /// - /// - /// Any .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. - /// - public class CsprojBuildDebuggeeTestStep : DotNetBuildDebuggeeTestStep - { - /// - /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee. - /// - /// - /// The runtime moniker to be built. - /// - public CsprojBuildDebuggeeTestStep(string dotnetToolPath, - string templateSolutionDirPath, - string debuggeeNativeLibDirPath, - Dictionary buildProperties, - string runtimeIdentifier, - string linkerPackageVersion, - string debuggeeName, - string debuggeeSolutionDirPath, - string debuggeeProjectDirPath, - string debuggeeBinaryDirPath, - string debuggeeBinaryDllPath, - string debuggeeBinaryExePath, - string nugetPackageCacheDirPath, - Dictionary 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; - } - - /// - /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee. - /// - public IDictionary BuildProperties { get; } - public string RuntimeIdentifier { get; } - public string DebuggeeName { get; } - public string LinkerPackageVersion { get; } - public override string ProjectTemplateFileName { get; } - - protected override async Task Restore(ITestOutputHelper output) - { - string extraArgs = ""; - if (RuntimeIdentifier != null) - { - extraArgs = " --runtime " + RuntimeIdentifier; - } - foreach (var prop in BuildProperties) - { - extraArgs += $" /p:{prop.Key}={prop.Value}"; - } - await Restore(extraArgs, output); - } - - protected override async Task Build(ITestOutputHelper output) - { - string publishArgs = "publish"; - if (RuntimeIdentifier != null) - { - publishArgs += " --runtime " + RuntimeIdentifier; - } - foreach (var prop in BuildProperties) - { - publishArgs += $" /p:{prop.Key}={prop.Value}"; - } - await Build(publishArgs, output); - } - - protected override void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output) - { - ConvertCsprojTemplate(filePath, Path.Combine(destDirPath, DebuggeeName + ".csproj")); - } - - private void ConvertCsprojTemplate(string csprojTemplatePath, string csprojOutPath) - { - var xdoc = XDocument.Load(csprojTemplatePath); - var ns = xdoc.Root.GetDefaultNamespace(); - if (LinkerPackageVersion != null) - { - AddLinkerPackageReference(xdoc, ns, LinkerPackageVersion); - } - using (var fs = new FileStream(csprojOutPath, FileMode.Create)) - { - xdoc.Save(fs); - } - } - - private static void AddLinkerPackageReference(XDocument xdoc, XNamespace ns, string linkerPackageVersion) - { - xdoc.Root.Add(new XElement(ns + "ItemGroup", - new XElement(ns + "PackageReference", - new XAttribute("Include", "ILLink.Tasks"), - new XAttribute("Version", linkerPackageVersion)))); - } - - protected override void AssertDebuggeeAssetsFileExists(ITestOutputHelper output) - { - AssertX.FileExists("debuggee project.assets.json", Path.Combine(DebuggeeProjectDirPath, "obj", "project.assets.json"), output); - } - - protected override void AssertDebuggeeProjectFileExists(ITestOutputHelper output) - { - AssertX.FileExists("debuggee csproj", Path.Combine(DebuggeeProjectDirPath, DebuggeeName + ".csproj"), output); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs deleted file mode 100644 index 725bca9f3..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/DebuggeeCompiler.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// 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. - /// - public static class DebuggeeCompiler - { - async public static Task 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 Execute(ITestOutputHelper output); - } - - public class DebuggeeConfiguration - { - public DebuggeeConfiguration(string sourcePath, string binaryDirPath, string binaryExePath) - { - SourcePath = sourcePath; - BinaryDirPath = binaryDirPath; - BinaryExePath = binaryExePath; - } - public string SourcePath { get; private set; } - public string BinaryDirPath { get; private set; } - public string BinaryExePath { get; private set; } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs deleted file mode 100644 index 70028705d..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/DotNetBuildDebuggeeTestStep.cs +++ /dev/null @@ -1,363 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// This test step builds debuggees using the dotnet tools - /// - /// - /// 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 - /// - public abstract class DotNetBuildDebuggeeTestStep : TestStep - { - /// - /// Create a new DotNetBuildDebuggeeTestStep. - /// - /// - /// The path to the dotnet executable - /// - /// - /// 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. - /// - /// - /// The path where the debuggee's native binary dependencies will be copied from. - /// - /// - /// The path where the debuggee solution will be created. For single project solutions this will be identical to - /// the debuggee project directory. - /// - /// - /// 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. - /// - /// - /// The directory path where the dotnet tool will place the compiled debuggee binaries. - /// - /// - /// The path where the dotnet tool will place the compiled debuggee assembly. - /// - /// - /// The path to which the build will copy the debuggee binary dll with a .exe extension. - /// - /// - /// 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. - /// - /// - /// A mapping of nuget feed names to locations. These feeds will be used to restore debuggee - /// nuget package dependencies. - /// - /// - /// The path where the build output will be logged - /// - public DotNetBuildDebuggeeTestStep(string dotnetToolPath, - string templateSolutionDirPath, - string debuggeeNativeLibDirPath, - string debuggeeSolutionDirPath, - string debuggeeProjectDirPath, - string debuggeeBinaryDirPath, - string debuggeeBinaryDllPath, - string debuggeeBinaryExePath, - string nugetPackageCacheDirPath, - Dictionary 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"); - } - } - - /// - /// The path to the dotnet executable - /// - public string DotNetToolPath { get; private set; } - /// - /// 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. - /// - public string DebuggeeTemplateSolutionDirPath { get; private set; } - /// - /// The path where the debuggee's native binary dependencies will be copied from. - /// - public string DebuggeeNativeLibDirPath { get; private set; } - /// - /// The path where the debuggee solution will be created. For single project solutions this will be identical to - /// the debuggee project directory. - /// - public string DebuggeeSolutionDirPath { get; private set; } - /// - /// 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. - /// - public string DebuggeeProjectDirPath { get; private set; } - /// - /// The directory path where the dotnet tool will place the compiled debuggee binaries. - /// - public string DebuggeeBinaryDirPath { get; private set; } - /// - /// The path where the dotnet tool will place the compiled debuggee assembly. - /// - public string DebuggeeBinaryDllPath { get; private set; } - /// - /// The path to which the build will copy the debuggee binary dll with a .exe extension. - /// - 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 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(""); - sb.AppendLine(""); - if(NugetFeeds != null && NugetFeeds.Count > 0) - { - sb.AppendLine(" "); - sb.AppendLine(" "); - foreach(KeyValuePair kv in NugetFeeds) - { - sb.AppendLine(" "); - } - sb.AppendLine(" "); - sb.AppendLine(" "); - sb.AppendLine(" "); - sb.AppendLine(" "); - } - sb.AppendLine(""); - - output.WriteLine("Creating: " + NuGetConfigPath); - File.WriteAllText(NuGetConfigPath, sb.ToString()); - } - - protected void AssertDebuggeeSolutionTemplateDirExists(ITestOutputHelper output) - { - AssertX.DirectoryExists("debuggee solution template directory", DebuggeeTemplateSolutionDirPath, output); - } - - protected void AssertDebuggeeProjectDirExists(ITestOutputHelper output) - { - AssertX.DirectoryExists("debuggee project directory", DebuggeeProjectDirPath, output); - } - - protected void AssertDebuggeeSolutionDirExists(ITestOutputHelper output) - { - AssertX.DirectoryExists("debuggee solution directory", DebuggeeSolutionDirPath, output); - } - - protected void AssertDebuggeeDllExists(ITestOutputHelper output) - { - AssertX.FileExists("debuggee dll", DebuggeeBinaryDllPath, output); - } - - protected void AssertDebuggeeExeExists(ITestOutputHelper output) - { - AssertX.FileExists("debuggee exe", DebuggeeBinaryExePath, output); - } - - protected abstract void AssertDebuggeeAssetsFileExists(ITestOutputHelper output); - - protected abstract void AssertDebuggeeProjectFileExists(ITestOutputHelper output); - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs deleted file mode 100644 index 58f7c182f..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/FileTestOutputHelper.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// An ITestOutputHelper implementation that logs to a file - /// - public class FileTestOutputHelper : ITestOutputHelper, IDisposable - { - readonly StreamWriter _logWriter; - readonly object _lock; - - public FileTestOutputHelper(string logFilePath, FileMode fileMode = FileMode.Create) - { - Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); - FileStream fs = new FileStream(logFilePath, fileMode); - _logWriter = new StreamWriter(fs); - _logWriter.AutoFlush = true; - _lock = new object(); - } - - public void WriteLine(string message) - { - lock (_lock) - { - _logWriter.WriteLine(message); - } - } - - public void WriteLine(string format, params object[] args) - { - lock (_lock) - { - _logWriter.WriteLine(format, args); - } - } - - public void Dispose() - { - lock (_lock) - { - _logWriter.Dispose(); - } - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs b/src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs deleted file mode 100644 index 709752578..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/IProcessLogger.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.Diagnostic.TestHelpers -{ - public enum ProcessStream - { - StandardIn = 0, - StandardOut = 1, - StandardError = 2, - MaxStreams = 3 - } - - public enum KillReason - { - TimedOut, - Unknown - } - - public interface IProcessLogger - { - void ProcessExited(ProcessRunner runner); - void ProcessKilled(ProcessRunner runner, KillReason reason); - void ProcessStarted(ProcessRunner runner); - void Write(ProcessRunner runner, string data, ProcessStream stream); - void WriteLine(ProcessRunner runner, string data, ProcessStream stream); - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs deleted file mode 100644 index 49f3a225c..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/IndentedTestOutputHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// An implementation of ITestOutputHelper that adds one indent level to - /// the start of each line - /// - public class IndentedTestOutputHelper : ITestOutputHelper - { - readonly string _indentText; - readonly ITestOutputHelper _output; - - public IndentedTestOutputHelper(ITestOutputHelper innerOutput, string indentText = " ") - { - _output = innerOutput; - _indentText = indentText; - } - - public void WriteLine(string message) - { - _output.WriteLine(_indentText + message); - } - - public void WriteLine(string format, params object[] args) - { - _output.WriteLine(_indentText + format, args); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj b/src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj deleted file mode 100644 index 0800c15f0..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Microsoft.Diagnostic.TestHelpers.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp2.0 - true - ;1591;1701 - true - Diagnostic test support - $(Description) - tests - embedded - - - - - - - diff --git a/src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs b/src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs deleted file mode 100644 index 781f1d15b..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/MultiplexTestOutputHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public class MultiplexTestOutputHelper : ITestOutputHelper - { - readonly ITestOutputHelper[] _outputs; - - public MultiplexTestOutputHelper(params ITestOutputHelper[] outputs) - { - _outputs = outputs; - } - - public void WriteLine(string message) - { - foreach(ITestOutputHelper output in _outputs) - { - output.WriteLine(message); - } - } - - public void WriteLine(string format, params object[] args) - { - foreach (ITestOutputHelper output in _outputs) - { - output.WriteLine(format, args); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs b/src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs deleted file mode 100644 index 6e0ae72b8..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/PrebuiltDebuggeeCompiler.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.IO; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public class PrebuiltDebuggeeCompiler : IDebuggeeCompiler - { - string _sourcePath; - string _binaryPath; - string _binaryExePath; - - public PrebuiltDebuggeeCompiler(TestConfiguration config, string debuggeeName) - { - //we anticipate paths like this: - //Source: //[] - //Binaries: // - _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 Execute(ITestOutputHelper output) - { - return Task.Factory.StartNew(() => new DebuggeeConfiguration(_sourcePath, _binaryPath, _binaryExePath)); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs b/src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs deleted file mode 100644 index 1e2404880..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/ProcessRunner.cs +++ /dev/null @@ -1,469 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// Executes a process and logs the output - /// - /// - /// 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. - /// - 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 _loggers; - Process _p; - DateTime _startTime; - TimeSpan _timeout; - ITestOutputHelper _traceOutput; - int? _expectedExitCode; - TaskCompletionSource _waitForProcessStartTaskSource; - Task _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(); - _timeout = TimeSpan.FromMinutes(10); - _cancelSource = new CancellationTokenSource(); - _killReason = null; - _waitForProcessStartTaskSource = new TaskCompletionSource(); - Task 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 EnvironmentVariables - { - get { lock (_lock) { return new Dictionary(_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 Run() - { - Start(); - return WaitForExit(); - } - - public Task 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 InternalWaitForExit(Task startProcessTask, Task stdOutTask, Task stdErrTask) - { - DebugTrace("starting InternalWaitForExit"); - Process p = await startProcessTask; - DebugTrace("InternalWaitForExit {0} '{1}'", p.Id, _replayCommand); - - Task processExit = Task.Factory.StartNew(() => - { - DebugTrace("starting Process.WaitForExit {0}", p.Id); - p.WaitForExit(); - DebugTrace("ending Process.WaitForExit {0}", p.Id); - }, - TaskCreationOptions.LongRunning); - - DebugTrace("awaiting process {0} exit, stdOut, and stdErr", p.Id); - await Task.WhenAll(processExit, stdOutTask, stdErrTask); - DebugTrace("await process {0} exit, stdOut, and stdErr complete", p.Id); - - foreach (IProcessLogger logger in Loggers) - { - logger.ProcessExited(this); - } - - lock (_lock) - { - if (_expectedExitCode.HasValue && p.ExitCode != _expectedExitCode.Value) - { - throw new Exception("Process returned exit code " + p.ExitCode + ", expected " + _expectedExitCode.Value + Environment.NewLine + - "Command Line: " + ReplayCommand + Environment.NewLine + - "Working Directory: " + WorkingDirectory); - } - DebugTrace("InternalWaitForExit {0} returning {1}", p.Id, p.ExitCode); - return p.ExitCode; - } - } - - private void DebugTrace(string format, params object[] args) - { - lock (_lock) - { - if (_traceOutput != null) - { - string message = string.Format(format, args); - _traceOutput.WriteLine("TRACE: {0}", message); - } - } - } - - class ConsoleTestOutputHelper : ITestOutputHelper - { - readonly ITestOutputHelper _output; - - public ConsoleTestOutputHelper(ITestOutputHelper output) - { - _output = output; - } - - public void WriteLine(string message) - { - Console.WriteLine(message); - if (_output != null) - { - _output.WriteLine(message); - } - - } - - public void WriteLine(string format, params object[] args) - { - Console.WriteLine(format, args); - if (_output != null) - { - _output.WriteLine(format, args); - } - } - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs b/src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs deleted file mode 100644 index 17d5a599c..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/TestConfiguration.cs +++ /dev/null @@ -1,702 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Xml.Linq; -using Xunit; -using Xunit.Extensions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// Represents the all the test configurations for a test run. - /// - public class TestRunConfiguration : IDisposable - { - public static TestRunConfiguration Instance - { - get { return _instance.Value; } - } - - static Lazy _instance = new Lazy(() => 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 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 initialConfig = new Dictionary - { - ["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> configs = ParseConfigFile(path, new Dictionary[] { initialConfig }); - Configurations = configs.Select(c => new TestConfiguration(c)); - } - - Dictionary[] ParseConfigFile(string path, Dictionary[] 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[] ParseConfigSettings(Dictionary[] templates, XElement node) - { - Dictionary[] currentTemplates = templates; - foreach (XElement child in node.Elements()) - { - currentTemplates = ParseConfigSetting(currentTemplates, child); - } - return currentTemplates; - } - - Dictionary[] ParseConfigSetting(Dictionary[] 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 currentTemplate = templates.Last(); - - switch (node.Name.LocalName) - { - case "Options": - if (EvaluateConditional(currentTemplate, node)) - { - List> newTemplates = new List>(); - foreach (XElement optionNode in node.Elements("Option")) - { - if (EvaluateConditional(currentTemplate, optionNode)) - { - IEnumerable> templateCopy = templates.Select(c => new Dictionary(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 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 config, XElement node) - { - foreach (XAttribute attr in node.Attributes("Condition")) - { - string conditionText = attr.Value; - - // Check if Exists('') - 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 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() - { - } - } - - /// - /// Represents the current test configuration - /// - public class TestConfiguration - { - const string DebugTypeKey = "DebugType"; - const string DebuggeeBuildRootKey = "DebuggeeBuildRoot"; - - internal static readonly string BaseDir = Path.GetFullPath("."); - - private Dictionary _settings; - - public TestConfiguration() - { - _settings = new Dictionary(); - } - - public TestConfiguration(Dictionary initialSettings) - { - _settings = new Dictionary(initialSettings); - } - - public IReadOnlyDictionary AllSettings - { - get { return _settings; } - } - - public TestConfiguration CloneWithNewDebugType(string pdbType) - { - Debug.Assert(!string.IsNullOrWhiteSpace(pdbType)); - - var currentSettings = new Dictionary(_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); - } - - /// - /// 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. - /// - public string TargetArchitecture - { - get { return GetValue("TargetArchitecture").ToLowerInvariant(); } - } - - /// - /// Built for "Debug" or "Release". Can be null. - /// - public string TargetConfiguration - { - get { return GetValue("TargetConfiguration"); } - } - - /// - /// The product "projectk" (.NET Core) or "desktop". - /// - public string TestProduct - { - get { return GetValue("TestProduct").ToLowerInvariant(); } - } - - /// - /// Returns true if running on .NET Core (based on TestProduct). - /// - public bool IsNETCore - { - get { return TestProduct.Equals("projectk"); } - } - - /// - /// Returns true if running on desktop framework (based on TestProduct). - /// - public bool IsDesktop - { - get { return TestProduct.Equals("desktop"); } - } - - /// - /// The test runner script directory - /// - public string ScriptRootDir - { - get { return MakeCanonicalPath(GetValue("ScriptRootDir")); } - } - - /// - /// Working temporary directory. - /// - public string WorkingDir - { - get { return MakeCanonicalPath(GetValue("WorkingDir")); } - } - - /// - /// The host program to run a .NET Core or null for desktop/no host. - /// - public string HostExe - { - get { return MakeCanonicalExePath(GetValue("HostExe")); } - } - - /// - /// Arguments to the HostExe. - /// - public string HostArgs - { - get { return GetValue("HostArgs"); } - } - - /// - /// Environment variables to pass to the target process (via the ProcessRunner). - /// - public string HostEnvVars - { - get { return GetValue("HostEnvVars"); } - } - - /// - /// Add the host environment variables to the process runner. - /// - /// process runner instance - 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]); - } - } - } - - /// - /// The directory to the runtime (coreclr.dll, etc.) symbols - /// - public string RuntimeSymbolsPath - { - get { return MakeCanonicalPath(GetValue("RuntimeSymbolsPath")); } - } - - /// - /// How the debuggees are built: "prebuilt" or "cli" (builds the debuggee during the test run with build and cli configuration). - /// - public string DebuggeeBuildProcess - { - get { return GetValue("DebuggeeBuildProcess")?.ToLowerInvariant(); } - } - - /// - /// Debuggee sources and template project file will be retrieved from here: //[] - /// - public string DebuggeeSourceRoot - { - get { return MakeCanonicalPath(GetValue("DebuggeeSourceRoot")); } - } - - /// - /// Debuggee final sources/project file/binary outputs will be placed here: // - /// - public string DebuggeeBuildRoot - { - get { return MakeCanonicalPath(GetValue(DebuggeeBuildRootKey)); } - } - - /// - /// Debuggee native binary dependencies will be retrieved from here. - /// - public string DebuggeeNativeLibRoot - { - get { return MakeCanonicalPath(GetValue("DebuggeeNativeLibRoot")); } - } - - /// - /// The version of the Microsoft.NETCore.App package to reference when building the debuggee. - /// - public string BuildProjectMicrosoftNetCoreAppVersion - { - get { return GetValue("BuildProjectMicrosoftNetCoreAppVersion"); } - } - - /// - /// The framework type/version used to build the debuggee like "netcoreapp2.0" or "netstandard1.0". - /// - public string BuildProjectFramework - { - get { return GetValue("BuildProjectFramework"); } - } - - /// - /// 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. - /// - public string BuildProjectRuntime - { - get { return GetValue("BuildProjectRuntime"); } - } - - /// - /// The version of the Microsoft.NETCore.App package to reference when running the debuggee (i.e. - /// using the dotnet cli --fx-version option). - /// - public string RuntimeFrameworkVersion - { - get { return GetValue("RuntimeFrameworkVersion"); } - } - - /// - /// The major portion of the runtime framework version - /// - 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"); - } - } - - /// - /// The type of PDB: "full" (Windows PDB) or "portable". - /// - public string DebugType - { - get { return GetValue(DebugTypeKey); } - } - - /// - /// Either the local path to the dotnet cli to build or the URL of the runtime to download and install. - /// - public string CliPath - { - get { return MakeCanonicalPath(GetValue("CliPath")); } - } - - /// - /// The local path to put the downloaded and decompressed runtime. - /// - public string CliCacheRoot - { - get { return MakeCanonicalPath(GetValue("CliCacheRoot")); } - } - - /// - /// The version (i.e. 2.0.0) of the dotnet cli to use. - /// - public string CliVersion - { - get { return GetValue("CliVersion"); } - } - - /// - /// The directory to cache the nuget packages on restore - /// - public string NuGetPackageCacheDir - { - get { return MakeCanonicalPath(GetValue("NuGetPackageCacheDir")); } - } - - /// - /// The nuget package feeds separated by semicolons. - /// - public string NuGetPackageFeeds - { - get { return GetValue("NuGetPackageFeeds"); } - } - - /// - /// If true, log the test output, etc. to the console. - /// - public bool LogToConsole - { - get { return bool.TryParse(GetValue("LogToConsole"), out bool b) && b; } - } - - /// - /// The directory to put the test logs. - /// - public string LogDirPath - { - get { return MakeCanonicalPath(GetValue("LogDir")); } - } - - /// - /// The "ILLink.Tasks" package version to reference or null. - /// - public string LinkerPackageVersion - { - get { return GetValue("LinkerPackageVersion"); } - } - - #region Runtime Features properties - - /// - /// Returns true if the "createdump" facility exists. - /// - public bool CreateDumpExists - { - get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor > 1; } - } - - /// - /// Returns true if a stack overflow causes dump to be generated with createdump. 3.x has now started to - /// create dumps on stack overflow. - /// - public bool StackOverflowCreatesDump - { - get { return IsNETCore && RuntimeFrameworkVersionMajor >= 3; } - } - - /// - /// Returns true if a stack overflow causes a SIGSEGV exception instead of aborting. - /// - public bool StackOverflowSIGSEGV - { - get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor == 1; } - } - - #endregion - - /// - /// Returns the configuration value for the key or null. - /// - /// name of the configuration value - /// configuration value or null - 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; - } - } - - /// - /// The OS running - /// - public enum OSKind - { - Windows, - Linux, - OSX, - Unknown, - } - - /// - /// The OS specific configuration - /// - 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; - } - } - - /// - /// The OS the tests are running. - /// - public static OSKind Kind { get; private set; } - - /// - /// 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. - /// - public static Architecture TargetArchitecture { get { return RuntimeInformation.ProcessArchitecture; } } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs b/src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs deleted file mode 100644 index 177a7d79d..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/TestOutputProcessLogger.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public class TestOutputProcessLogger : IProcessLogger - { - string _timeFormat = "mm\\:ss\\.fff"; - ITestOutputHelper _output; - StringBuilder[] _lineBuffers; - - public TestOutputProcessLogger(ITestOutputHelper output) - { - _output = output; - _lineBuffers = new StringBuilder[(int)ProcessStream.MaxStreams]; - } - - public void ProcessStarted(ProcessRunner runner) - { - lock (this) - { - _output.WriteLine("Running Process: " + runner.ReplayCommand); - _output.WriteLine("Working Directory: " + runner.WorkingDirectory); - IEnumerable> additionalEnvVars = - runner.EnvironmentVariables.Where(kv => Environment.GetEnvironmentVariable(kv.Key) != kv.Value); - - if(additionalEnvVars.Any()) - { - _output.WriteLine("Additional Environment Variables: " + - string.Join(", ", additionalEnvVars.Select(kv => kv.Key + "=" + kv.Value))); - } - _output.WriteLine("{"); - } - } - - public virtual void Write(ProcessRunner runner, string data, ProcessStream stream) - { - lock (this) - { - AppendToLineBuffer(runner, stream, data); - } - } - - public virtual void WriteLine(ProcessRunner runner, string data, ProcessStream stream) - { - lock (this) - { - StringBuilder lineBuffer = AppendToLineBuffer(runner, stream, data); - //Ensure all output is written even if it isn't a full line before we log input - if (stream == ProcessStream.StandardIn) - { - FlushOutput(); - } - _output.WriteLine(lineBuffer.ToString()); - _lineBuffers[(int)stream] = null; - } - } - - public virtual void ProcessExited(ProcessRunner runner) - { - lock (this) - { - TimeSpan offset = runner.StartTime - DateTime.Now; - _output.WriteLine("}"); - _output.WriteLine("Exit code: " + runner.ExitCode + " ( " + offset.ToString(_timeFormat) + " elapsed)"); - _output.WriteLine(""); - } - } - - public void ProcessKilled(ProcessRunner runner, KillReason reason) - { - lock (this) - { - TimeSpan offset = runner.StartTime - DateTime.Now; - string reasonText = ""; - if (reason == KillReason.TimedOut) - { - reasonText = "Process timed out"; - } - else if (reason == KillReason.Unknown) - { - reasonText = "Kill() was called"; - } - _output.WriteLine(" Killing process: " + offset.ToString(_timeFormat) + ": " + reasonText); - } - } - - protected void FlushOutput() - { - if (_lineBuffers[(int)ProcessStream.StandardOut] != null) - { - _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardOut].ToString()); - _lineBuffers[(int)ProcessStream.StandardOut] = null; - } - if (_lineBuffers[(int)ProcessStream.StandardError] != null) - { - _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardError].ToString()); - _lineBuffers[(int)ProcessStream.StandardError] = null; - } - } - - private StringBuilder AppendToLineBuffer(ProcessRunner runner, ProcessStream stream, string data) - { - StringBuilder lineBuffer = _lineBuffers[(int)stream]; - if (lineBuffer == null) - { - TimeSpan offset = runner.StartTime - DateTime.Now; - lineBuffer = new StringBuilder(); - lineBuffer.Append(" "); - if (stream == ProcessStream.StandardError) - { - lineBuffer.Append("STDERROR: "); - } - else if (stream == ProcessStream.StandardIn) - { - lineBuffer.Append("STDIN: "); - } - lineBuffer.Append(offset.ToString(_timeFormat)); - lineBuffer.Append(": "); - _lineBuffers[(int)stream] = lineBuffer; - } - - // xunit has a bug where a non-printable character isn't properly escaped when - // it is written into the xml results which ultimately results in - // the xml being improperly truncated. For example MDbg has a test case that prints - // \0 and dotnet tools print \u001B to colorize their console output. - foreach(char c in data) - { - if(!char.IsControl(c)) - { - lineBuffer.Append(c); - } - } - return lineBuffer; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs b/src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs deleted file mode 100644 index 0bc1b6ef4..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/TestRunner.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - public class TestRunner - { - /// - /// Run debuggee (without any debugger) and compare the console output to the regex specified. - /// - /// test config to use - /// output helper - /// test case name - /// debuggee name (no path) - /// regex to match on console (standard and error) output - /// - public static async Task 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(); - } - } - - /// - /// Returns a test config for each PDB type supported by the product/platform. - /// - /// starting config - /// new configs for each supported PDB type - public static IEnumerable 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); - } - } - } - - /// - /// Returns an output helper for the specified config. - /// - /// test config - /// starting output helper - /// test case name - /// new output helper - public static TestRunner.OutputHelper ConfigureLogging(TestConfiguration config, ITestOutputHelper output, string testName) - { - FileTestOutputHelper fileLogger = null; - ConsoleTestOutputHelper consoleLogger = null; - if (!string.IsNullOrEmpty(config.LogDirPath)) - { - string logFileName = testName + "." + config.ToString() + ".log"; - string logPath = Path.Combine(config.LogDirPath, logFileName); - fileLogger = new FileTestOutputHelper(logPath, FileMode.Append); - } - if (config.LogToConsole) - { - consoleLogger = new ConsoleTestOutputHelper(); - } - return new TestRunner.OutputHelper(output, fileLogger, consoleLogger); - } - - public class OutputHelper : ITestOutputHelper, IDisposable - { - readonly ITestOutputHelper _output; - readonly FileTestOutputHelper _fileLogger; - readonly ConsoleTestOutputHelper _consoleLogger; - - public readonly ITestOutputHelper IndentedOutput; - - public OutputHelper(ITestOutputHelper output, FileTestOutputHelper fileLogger, ConsoleTestOutputHelper consoleLogger) - { - _output = output; - _fileLogger = fileLogger; - _consoleLogger = consoleLogger; - IndentedOutput = new IndentedTestOutputHelper(this); - } - - public void WriteLine(string message) - { - _output.WriteLine(message); - _fileLogger?.WriteLine(message); - _consoleLogger?.WriteLine(message); - } - - public void WriteLine(string format, params object[] args) - { - _output.WriteLine(format, args); - _fileLogger?.WriteLine(format, args); - _consoleLogger?.WriteLine(format, args); - } - - public void Dispose() - { - _fileLogger?.Dispose(); - } - } - - public class TestLogger : TestOutputProcessLogger - { - readonly StringBuilder _standardOutput; - readonly StringBuilder _standardError; - - public TestLogger(ITestOutputHelper output) - : base(output) - { - lock (this) - { - _standardOutput = new StringBuilder(); - _standardError = new StringBuilder(); - } - } - - public string GetStandardOutput() - { - lock (this) - { - return _standardOutput.ToString(); - } - } - - public string GetStandardError() - { - lock (this) - { - return _standardError.ToString(); - } - } - - public override void Write(ProcessRunner runner, string data, ProcessStream stream) - { - lock (this) - { - base.Write(runner, data, stream); - switch (stream) - { - case ProcessStream.StandardOut: - _standardOutput.Append(data); - break; - - case ProcessStream.StandardError: - _standardError.Append(data); - break; - } - } - } - - public override void WriteLine(ProcessRunner runner, string data, ProcessStream stream) - { - lock (this) - { - base.WriteLine(runner, data, stream); - switch (stream) - { - case ProcessStream.StandardOut: - _standardOutput.AppendLine(data); - break; - - case ProcessStream.StandardError: - _standardError.AppendLine(data); - break; - } - } - } - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/TestStep.cs b/src/Microsoft.Diagnostic.TestHelpers/TestStep.cs deleted file mode 100644 index 1931fdd7f..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/TestStep.cs +++ /dev/null @@ -1,636 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostic.TestHelpers -{ - /// - /// 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 - /// - 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 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(""); - if(indexOfInitialStepStateElementEnd == -1) - { - return false; - } - int splitIndex = indexOfInitialStepStateElementEnd + "".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(runStateElement.Value, out runState)) - { - return; - } - DateTimeOffset? completeTime = null; - XElement completeTimeElement = finalTaskStateElement.Element("CompleteTime"); - if (completeTimeElement != null) - { - DateTimeOffset tempCompleteTime; - if (!DateTimeOffset.TryParse(completeTimeElement.Value, out tempCompleteTime)) - { - return; - } - else - { - completeTime = tempCompleteTime; - } - } - XElement errorMessageElement = finalTaskStateElement.Element("ErrorMessage"); - string errorMessage = null; - if (errorMessageElement != null) - { - errorMessage = errorMessageElement.Value; - } - XElement errorStackTraceElement = finalTaskStateElement.Element("ErrorStackTrace"); - string errorStackTrace = null; - if (errorStackTraceElement != null) - { - errorStackTrace = errorStackTraceElement.Value; - } - - taskState = taskState.WithFinalState(runState, completeTime, errorMessage, errorStackTrace); - } - catch (XmlException) { } - } - } - } - - public class TestStepException : Exception - { - public TestStepException(string errorMessage) : - base(errorMessage) - { } - - public TestStepException(string stepName, string errorMessage, string stackTrace) : - base("The " + stepName + " test step failed." + Environment.NewLine + - "Original Error: " + errorMessage + Environment.NewLine + - stackTrace) - { } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs deleted file mode 100644 index 21c5dbf6e..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkipTestException.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Xunit.Extensions -{ - public class SkipTestException : Exception - { - public SkipTestException(string reason) - : base(reason) { } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs deleted file mode 100644 index 8421204f6..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Xunit; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostic.TestHelpers")] - public class SkippableFactAttribute : FactAttribute { } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs deleted file mode 100644 index b24916055..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer - { - readonly IMessageSink diagnosticMessageSink; - - public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink) - { - this.diagnosticMessageSink = diagnosticMessageSink; - } - - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - yield return new SkippableFactTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs deleted file mode 100644 index 9a7fa3074..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - public class SkippableFactMessageBus : IMessageBus - { - readonly IMessageBus innerBus; - - public SkippableFactMessageBus(IMessageBus innerBus) - { - this.innerBus = innerBus; - } - - public int DynamicallySkippedTestCount { get; private set; } - - public void Dispose() { } - - public bool QueueMessage(IMessageSinkMessage message) - { - var testFailed = message as ITestFailed; - if (testFailed != null) - { - var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); - if (exceptionType == typeof(SkipTestException).FullName) - { - DynamicallySkippedTestCount++; - return innerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); - } - } - - // Nothing we care about, send it on its way - return innerBus.QueueMessage(message); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs deleted file mode 100644 index e031171d9..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - public class SkippableFactTestCase : XunitTestCase - { - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableFactTestCase() { } - - public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { } - - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableFactMessageBus(messageBus); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource); - if (skipMessageBus.DynamicallySkippedTestCount > 0) - { - result.Failed -= skipMessageBus.DynamicallySkippedTestCount; - result.Skipped += skipMessageBus.DynamicallySkippedTestCount; - } - - return result; - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs deleted file mode 100644 index 5496ea45b..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostic.TestHelpers")] - public class SkippableTheoryAttribute : TheoryAttribute { } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs deleted file mode 100644 index 40c4ab7dd..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - public class SkippableTheoryDiscoverer : IXunitTestCaseDiscoverer - { - readonly IMessageSink diagnosticMessageSink; - readonly TheoryDiscoverer theoryDiscoverer; - - public SkippableTheoryDiscoverer(IMessageSink diagnosticMessageSink) - { - this.diagnosticMessageSink = diagnosticMessageSink; - - theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink); - } - - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - var defaultMethodDisplay = discoveryOptions.MethodDisplayOrDefault(); - var defaultMethodDisplayOptions = discoveryOptions.MethodDisplayOptionsOrDefault(); - - // Unlike fact discovery, the underlying algorithm for theories is complex, so we let the theory discoverer - // do its work, and do a little on-the-fly conversion into our own test cases. - return theoryDiscoverer.Discover(discoveryOptions, testMethod, factAttribute) - .Select(testCase => testCase is XunitTheoryTestCase - ? (IXunitTestCase)new SkippableTheoryTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod) - : new SkippableFactTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod, testCase.TestMethodArguments)); - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs deleted file mode 100644 index 46d2037ee..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Xunit.Extensions -{ - public class SkippableTheoryTestCase : XunitTheoryTestCase - { - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTheoryTestCase() { } - - public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } - - public override async Task RunAsync(IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - // Duplicated code from SkippableFactTestCase. I'm sure we could find a way to de-dup with some thought. - var skipMessageBus = new SkippableFactMessageBus(messageBus); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource); - if (skipMessageBus.DynamicallySkippedTestCount > 0) - { - result.Failed -= skipMessageBus.DynamicallySkippedTestCount; - result.Skipped += skipMessageBus.DynamicallySkippedTestCount; - } - - return result; - } - } -} diff --git a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt b/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt deleted file mode 100644 index 39b2d6592..000000000 --- a/src/Microsoft.Diagnostic.TestHelpers/Xunit.Extensions/license.txt +++ /dev/null @@ -1,15 +0,0 @@ -Source in this directory is derived source at https://github.com/xunit/samples.xunit. The repo provided the following license: - -Copyright 2014 Outercurve Foundation - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs new file mode 100644 index 000000000..e5b088231 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/AcquireDotNetTestStep.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// Acquires the CLI tools from a web endpoint, a local zip/tar.gz, or directly from a local path + /// + public class AcquireDotNetTestStep : TestStep + { + /// + /// Create a new AcquireDotNetTestStep + /// + /// + /// 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 + /// + /// 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 + /// + /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar + /// file. Otherwise this path is unused. + /// + /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the + /// archive. Otherwise this path is unused. + /// + /// 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. + /// + /// The path where an activity log for this test step should be written. + /// + /// + 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; + } + + /// + /// 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. + /// + public string RemoteDotNetPath { get; private set; } + + /// + /// 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. + /// + public string LocalDotNetZipPath { get; private set; } + + /// + /// If localDotNetZipPath points to a .tar.gz, this path will be used to store the uncompressed .tar + /// file. Otherwise null. + /// + public string LocalDotNetTarPath { get; private set; } + + /// + /// If localDotNetZipPath is non-null, this path will be used to store the expanded version of the + /// archive. Otherwise null. + /// + public string LocalDotNetZipExpandDirPath { get; private set; } + + /// + /// The path to the dotnet binary when the test step is complete. + /// + public string LocalDotNetPath { get; private set; } + + /// + /// Returns true, if there any actual work to do (like downloading, unziping or untaring). + /// + public bool AnyWorkToDo { get { return RemoteDotNetPath != null || LocalDotNetZipPath != null; } } + + async protected override Task DoWork(ITestOutputHelper output) + { + if (RemoteDotNetPath != null) + { + await DownloadFile(RemoteDotNetPath, LocalDotNetZipPath, output); + } + if (LocalDotNetZipPath != null) + { + if (LocalDotNetZipPath.EndsWith(".zip")) + { + await Unzip(LocalDotNetZipPath, LocalDotNetZipExpandDirPath, output); + } + else if(LocalDotNetZipPath.EndsWith(".tar.gz")) + { + await UnGZip(LocalDotNetZipPath, LocalDotNetTarPath, output); + await Untar(LocalDotNetTarPath, LocalDotNetZipExpandDirPath, output); + } + else + { + output.WriteLine("Unsupported compression format: " + LocalDotNetZipPath); + throw new NotSupportedException("Unsupported compression format: " + LocalDotNetZipPath); + } + } + output.WriteLine("Dotnet path: " + LocalDotNetPath); + if (!File.Exists(LocalDotNetPath)) + { + throw new FileNotFoundException(LocalDotNetPath + " not found"); + } + } + + async static Task DownloadFile(string remotePath, string localPath, ITestOutputHelper output) + { + output.WriteLine("Downloading: " + remotePath + " -> " + localPath); + Directory.CreateDirectory(Path.GetDirectoryName(localPath)); + WebRequest request = HttpWebRequest.Create(remotePath); + WebResponse response = await request.GetResponseAsync(); + using (FileStream localZipStream = File.OpenWrite(localPath)) + { + // TODO: restore the CopyToAsync code after System.Net.Http.dll is + // updated to a newer version. The current old version has a bug + // where the copy never finished. + // await response.GetResponseStream().CopyToAsync(localZipStream); + byte[] buffer = new byte[16 * 1024]; + long bytesLeft = response.ContentLength; + + while (bytesLeft > 0) + { + int read = response.GetResponseStream().Read(buffer, 0, buffer.Length); + if (read == 0) + break; + localZipStream.Write(buffer, 0, read); + bytesLeft -= read; + } + output.WriteLine("Downloading finished"); + } + } + + async static Task UnGZip(string gzipPath, string expandedFilePath, ITestOutputHelper output) + { + output.WriteLine("Unziping: " + gzipPath + " -> " + expandedFilePath); + using (FileStream gzipStream = File.OpenRead(gzipPath)) + { + using (GZipStream expandedStream = new GZipStream(gzipStream, CompressionMode.Decompress)) + { + using (FileStream targetFileStream = File.OpenWrite(expandedFilePath)) + { + await expandedStream.CopyToAsync(targetFileStream); + } + } + } + } + + async static Task Unzip(string zipPath, string expandedDirPath, ITestOutputHelper output) + { + output.WriteLine("Unziping: " + zipPath + " -> " + expandedDirPath); + using (FileStream zipStream = File.OpenRead(zipPath)) + { + ZipArchive zip = new ZipArchive(zipStream); + foreach (ZipArchiveEntry entry in zip.Entries) + { + string extractedFilePath = Path.Combine(expandedDirPath, entry.FullName); + Directory.CreateDirectory(Path.GetDirectoryName(extractedFilePath)); + using (Stream zipFileStream = entry.Open()) + { + using (FileStream extractedFileStream = File.OpenWrite(extractedFilePath)) + { + await zipFileStream.CopyToAsync(extractedFileStream); + } + } + } + } + } + + async static Task Untar(string tarPath, string expandedDirPath, ITestOutputHelper output) + { + Directory.CreateDirectory(expandedDirPath); + string tarToolPath = null; + if (OS.Kind == OSKind.Linux) + { + tarToolPath = "/bin/tar"; + } + else if (OS.Kind == OSKind.OSX) + { + tarToolPath = "/usr/bin/tar"; + } + else + { + throw new NotSupportedException("Unknown where this OS stores the tar executable"); + } + + await new ProcessRunner(tarToolPath, "-xf " + tarPath). + WithWorkingDirectory(expandedDirPath). + WithLog(output). + WithExpectedExitCode(0). + Run(); + } + + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/AssertX.cs b/src/Microsoft.Diagnostics.TestHelpers/AssertX.cs new file mode 100644 index 000000000..a28b80b88 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/AssertX.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public static partial class AssertX + { + public static void DirectoryExists(string dirDescriptiveName, string dirPath, ITestOutputHelper output) + { + if (!Directory.Exists(dirPath)) + { + string errorMessage = "Expected " + dirDescriptiveName + " to exist: " + dirPath; + output.WriteLine(errorMessage); + try + { + string parentDir = dirPath; + while (true) + { + if (Directory.Exists(Path.GetDirectoryName(parentDir))) + { + output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir)); + break; + } + if (Path.GetDirectoryName(parentDir) == parentDir) + { + output.WriteLine("Also unable to find any parent directory that exists"); + break; + } + parentDir = Path.GetDirectoryName(parentDir); + } + } + catch (Exception e) + { + output.WriteLine("Additional error while trying to diagnose missing directory:"); + output.WriteLine(e.GetType() + ": " + e.Message); + } + throw new DirectoryNotFoundException(errorMessage); + } + } + + public static void FileExists(string fileDescriptiveName, string filePath, ITestOutputHelper output) + { + if (!File.Exists(filePath)) + { + string errorMessage = "Expected " + fileDescriptiveName + " to exist: " + filePath; + output.WriteLine(errorMessage); + try + { + string parentDir = filePath; + while (true) + { + if (Directory.Exists(Path.GetDirectoryName(parentDir))) + { + output.WriteLine("First parent directory that exists: " + Path.GetDirectoryName(parentDir)); + break; + } + if (Path.GetDirectoryName(parentDir) == parentDir) + { + output.WriteLine("Also unable to find any parent directory that exists"); + break; + } + parentDir = Path.GetDirectoryName(parentDir); + } + } + catch (Exception e) + { + output.WriteLine("Additional error while trying to diagnose missing file:"); + output.WriteLine(e.GetType() + ": " + e.Message); + } + throw new FileNotFoundException(errorMessage); + } + } + } +} + + diff --git a/src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs new file mode 100644 index 000000000..79d4ce3d9 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/BaseDebuggeeCompiler.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// This compiler acquires the CLI tools and uses them to build debuggees. + /// + /// + /// 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 + /// + public abstract class BaseDebuggeeCompiler : IDebuggeeCompiler + { + AcquireDotNetTestStep _acquireTask; + DotNetBuildDebuggeeTestStep _buildDebuggeeTask; + + /// + /// Creates a new BaseDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build debuggees via dotnet build. + /// + /// + /// 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 + /// + /// + /// 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 + /// + public BaseDebuggeeCompiler(TestConfiguration config, string debuggeeName) + { + _acquireTask = ConfigureAcquireDotNetTask(config); + _buildDebuggeeTask = ConfigureDotNetBuildDebuggeeTask(config, _acquireTask.LocalDotNetPath, config.CliVersion, debuggeeName); + } + + async public Task 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 GetNugetFeeds(TestConfiguration config) + { + Dictionary nugetFeeds = new Dictionary(); + 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 = 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: / + //DebuggeeNativeLibDir: / + //DotNetRootBuildDir: + //DebuggeeSolutionDir: / + //DebuggeeProjectDir: /[/] + //DebuggeeBinaryDir: /[/]/bin/Debug//[] + //DebuggeeBinaryDll: /[/]/bin/Debug//.dll + //DebuggeeBinaryExe: /[/]/bin/Debug//[]/.exe + //LogPath: /.txt + + // When the runtime directory is present it will have a native host exe in it that has been renamed to the debugee + // name. It also has a managed dll in it which functions as a managed exe when renamed. + // When the runtime directory is missing, the framework directory will have a managed dll in it that functions if it + // is renamed to an exe. I'm sure that renaming isn't the intended usage, but it works and produces less churn + // in our tests for the moment. + public abstract DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName); + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs new file mode 100644 index 000000000..c843ac17a --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/CliDebuggeeCompiler.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish. + /// + public class CliDebuggeeCompiler : BaseDebuggeeCompiler + { + /// + /// Creates a new CliDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish. + /// + /// LinkerPackageVersion If set, this version of the linker package will be used to link the debuggee during publish. + /// + /// + public CliDebuggeeCompiler(TestConfiguration config, string debuggeeName) : base(config, debuggeeName) {} + + private static Dictionary GetBuildProperties(TestConfiguration config, string runtimeIdentifier) + { + Dictionary buildProperties = new Dictionary(); + buildProperties.Add("RuntimeFrameworkVersion", config.BuildProjectMicrosoftNetCoreAppVersion); + buildProperties.Add("BuildProjectFramework", config.BuildProjectFramework); + if (runtimeIdentifier != null) + { + buildProperties.Add("RuntimeIdentifier", runtimeIdentifier); + } + string debugType = config.DebugType; + if (debugType == null) + { + // The default PDB type is portable + debugType = "portable"; + } + buildProperties.Add("DebugType", debugType); + return buildProperties; + } + + protected override string GetFramework(TestConfiguration config) + { + return config.BuildProjectFramework ?? "netcoreapp2.0"; + } + + protected override string GetDebuggeeBinaryDirPath(string debuggeeProjectDirPath, string framework, string runtime) + { + string debuggeeBinaryDirPath = base.GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtime); + debuggeeBinaryDirPath = Path.Combine(debuggeeBinaryDirPath, "publish"); + return debuggeeBinaryDirPath; + } + + public override DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName) + { + string runtimeIdentifier = GetRuntime(config); + string framework = GetFramework(config); + string initialSourceDirPath = GetInitialSourceDirPath(config, debuggeeName); + string dotNetRootBuildDirPath = GetDotNetRootBuildDirPath(config); + string debuggeeSolutionDirPath = GetDebuggeeSolutionDirPath(dotNetRootBuildDirPath, debuggeeName); + string debuggeeProjectDirPath = GetDebuggeeProjectDirPath(debuggeeSolutionDirPath, initialSourceDirPath, debuggeeName); + string debuggeeBinaryDirPath = GetDebuggeeBinaryDirPath(debuggeeProjectDirPath, framework, runtimeIdentifier); + string debuggeeBinaryDllPath = config.IsNETCore ? GetDebuggeeBinaryDllPath(debuggeeBinaryDirPath, debuggeeName) : null; + string debuggeeBinaryExePath = config.IsDesktop ? GetDebuggeeBinaryExePath(debuggeeBinaryDirPath, debuggeeName) : null; + string logPath = GetLogPath(config, framework, runtimeIdentifier, debuggeeName); + return new CsprojBuildDebuggeeTestStep(dotNetPath, + initialSourceDirPath, + GetDebuggeeNativeLibDirPath(config, debuggeeName), + GetBuildProperties(config, runtimeIdentifier), + runtimeIdentifier, + config.LinkerPackageVersion, + debuggeeName, + debuggeeSolutionDirPath, + debuggeeProjectDirPath, + debuggeeBinaryDirPath, + debuggeeBinaryDllPath, + debuggeeBinaryExePath, + config.NuGetPackageCacheDir, + GetNugetFeeds(config), + logPath); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs new file mode 100644 index 000000000..288d90ec3 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/ConsoleTestOutputHelper.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public class ConsoleTestOutputHelper : ITestOutputHelper + { + public void WriteLine(string message) + { + Console.WriteLine(message); + } + + public void WriteLine(string format, params object[] args) + { + Console.WriteLine(format, args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs new file mode 100644 index 000000000..7010938ad --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/CsprojBuildDebuggeeTestStep.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// This test step builds debuggees using the dotnet tools with .csproj projects files. + /// + /// + /// Any .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. + /// + public class CsprojBuildDebuggeeTestStep : DotNetBuildDebuggeeTestStep + { + /// + /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee. + /// + /// + /// The runtime moniker to be built. + /// + public CsprojBuildDebuggeeTestStep(string dotnetToolPath, + string templateSolutionDirPath, + string debuggeeNativeLibDirPath, + Dictionary buildProperties, + string runtimeIdentifier, + string linkerPackageVersion, + string debuggeeName, + string debuggeeSolutionDirPath, + string debuggeeProjectDirPath, + string debuggeeBinaryDirPath, + string debuggeeBinaryDllPath, + string debuggeeBinaryExePath, + string nugetPackageCacheDirPath, + Dictionary 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; + } + + /// + /// A mapping from .csproj property strings to their values. These properties will be set when building the debuggee. + /// + public IDictionary BuildProperties { get; } + public string RuntimeIdentifier { get; } + public string DebuggeeName { get; } + public string LinkerPackageVersion { get; } + public override string ProjectTemplateFileName { get; } + + protected override async Task Restore(ITestOutputHelper output) + { + string extraArgs = ""; + if (RuntimeIdentifier != null) + { + extraArgs = " --runtime " + RuntimeIdentifier; + } + foreach (var prop in BuildProperties) + { + extraArgs += $" /p:{prop.Key}={prop.Value}"; + } + await Restore(extraArgs, output); + } + + protected override async Task Build(ITestOutputHelper output) + { + string publishArgs = "publish"; + if (RuntimeIdentifier != null) + { + publishArgs += " --runtime " + RuntimeIdentifier; + } + foreach (var prop in BuildProperties) + { + publishArgs += $" /p:{prop.Key}={prop.Value}"; + } + await Build(publishArgs, output); + } + + protected override void ExpandProjectTemplate(string filePath, string destDirPath, ITestOutputHelper output) + { + ConvertCsprojTemplate(filePath, Path.Combine(destDirPath, DebuggeeName + ".csproj")); + } + + private void ConvertCsprojTemplate(string csprojTemplatePath, string csprojOutPath) + { + var xdoc = XDocument.Load(csprojTemplatePath); + var ns = xdoc.Root.GetDefaultNamespace(); + if (LinkerPackageVersion != null) + { + AddLinkerPackageReference(xdoc, ns, LinkerPackageVersion); + } + using (var fs = new FileStream(csprojOutPath, FileMode.Create)) + { + xdoc.Save(fs); + } + } + + private static void AddLinkerPackageReference(XDocument xdoc, XNamespace ns, string linkerPackageVersion) + { + xdoc.Root.Add(new XElement(ns + "ItemGroup", + new XElement(ns + "PackageReference", + new XAttribute("Include", "ILLink.Tasks"), + new XAttribute("Version", linkerPackageVersion)))); + } + + protected override void AssertDebuggeeAssetsFileExists(ITestOutputHelper output) + { + AssertX.FileExists("debuggee project.assets.json", Path.Combine(DebuggeeProjectDirPath, "obj", "project.assets.json"), output); + } + + protected override void AssertDebuggeeProjectFileExists(ITestOutputHelper output) + { + AssertX.FileExists("debuggee csproj", Path.Combine(DebuggeeProjectDirPath, DebuggeeName + ".csproj"), output); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs new file mode 100644 index 000000000..9dd2fced5 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/DebuggeeCompiler.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// 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. + /// + public static class DebuggeeCompiler + { + async public static Task 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 Execute(ITestOutputHelper output); + } + + public class DebuggeeConfiguration + { + public DebuggeeConfiguration(string sourcePath, string binaryDirPath, string binaryExePath) + { + SourcePath = sourcePath; + BinaryDirPath = binaryDirPath; + BinaryExePath = binaryExePath; + } + public string SourcePath { get; private set; } + public string BinaryDirPath { get; private set; } + public string BinaryExePath { get; private set; } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs new file mode 100644 index 000000000..6a3b7359c --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/DotNetBuildDebuggeeTestStep.cs @@ -0,0 +1,363 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// This test step builds debuggees using the dotnet tools + /// + /// + /// 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 + /// + public abstract class DotNetBuildDebuggeeTestStep : TestStep + { + /// + /// Create a new DotNetBuildDebuggeeTestStep. + /// + /// + /// The path to the dotnet executable + /// + /// + /// 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. + /// + /// + /// The path where the debuggee's native binary dependencies will be copied from. + /// + /// + /// The path where the debuggee solution will be created. For single project solutions this will be identical to + /// the debuggee project directory. + /// + /// + /// 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. + /// + /// + /// The directory path where the dotnet tool will place the compiled debuggee binaries. + /// + /// + /// The path where the dotnet tool will place the compiled debuggee assembly. + /// + /// + /// The path to which the build will copy the debuggee binary dll with a .exe extension. + /// + /// + /// 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. + /// + /// + /// A mapping of nuget feed names to locations. These feeds will be used to restore debuggee + /// nuget package dependencies. + /// + /// + /// The path where the build output will be logged + /// + public DotNetBuildDebuggeeTestStep(string dotnetToolPath, + string templateSolutionDirPath, + string debuggeeNativeLibDirPath, + string debuggeeSolutionDirPath, + string debuggeeProjectDirPath, + string debuggeeBinaryDirPath, + string debuggeeBinaryDllPath, + string debuggeeBinaryExePath, + string nugetPackageCacheDirPath, + Dictionary 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"); + } + } + + /// + /// The path to the dotnet executable + /// + public string DotNetToolPath { get; private set; } + /// + /// 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. + /// + public string DebuggeeTemplateSolutionDirPath { get; private set; } + /// + /// The path where the debuggee's native binary dependencies will be copied from. + /// + public string DebuggeeNativeLibDirPath { get; private set; } + /// + /// The path where the debuggee solution will be created. For single project solutions this will be identical to + /// the debuggee project directory. + /// + public string DebuggeeSolutionDirPath { get; private set; } + /// + /// 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. + /// + public string DebuggeeProjectDirPath { get; private set; } + /// + /// The directory path where the dotnet tool will place the compiled debuggee binaries. + /// + public string DebuggeeBinaryDirPath { get; private set; } + /// + /// The path where the dotnet tool will place the compiled debuggee assembly. + /// + public string DebuggeeBinaryDllPath { get; private set; } + /// + /// The path to which the build will copy the debuggee binary dll with a .exe extension. + /// + 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 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(""); + sb.AppendLine(""); + if(NugetFeeds != null && NugetFeeds.Count > 0) + { + sb.AppendLine(" "); + sb.AppendLine(" "); + foreach(KeyValuePair kv in NugetFeeds) + { + sb.AppendLine(" "); + } + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + } + sb.AppendLine(""); + + output.WriteLine("Creating: " + NuGetConfigPath); + File.WriteAllText(NuGetConfigPath, sb.ToString()); + } + + protected void AssertDebuggeeSolutionTemplateDirExists(ITestOutputHelper output) + { + AssertX.DirectoryExists("debuggee solution template directory", DebuggeeTemplateSolutionDirPath, output); + } + + protected void AssertDebuggeeProjectDirExists(ITestOutputHelper output) + { + AssertX.DirectoryExists("debuggee project directory", DebuggeeProjectDirPath, output); + } + + protected void AssertDebuggeeSolutionDirExists(ITestOutputHelper output) + { + AssertX.DirectoryExists("debuggee solution directory", DebuggeeSolutionDirPath, output); + } + + protected void AssertDebuggeeDllExists(ITestOutputHelper output) + { + AssertX.FileExists("debuggee dll", DebuggeeBinaryDllPath, output); + } + + protected void AssertDebuggeeExeExists(ITestOutputHelper output) + { + AssertX.FileExists("debuggee exe", DebuggeeBinaryExePath, output); + } + + protected abstract void AssertDebuggeeAssetsFileExists(ITestOutputHelper output); + + protected abstract void AssertDebuggeeProjectFileExists(ITestOutputHelper output); + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs new file mode 100644 index 000000000..1f4a39852 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/FileTestOutputHelper.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// An ITestOutputHelper implementation that logs to a file + /// + public class FileTestOutputHelper : ITestOutputHelper, IDisposable + { + readonly StreamWriter _logWriter; + readonly object _lock; + + public FileTestOutputHelper(string logFilePath, FileMode fileMode = FileMode.Create) + { + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); + FileStream fs = new FileStream(logFilePath, fileMode); + _logWriter = new StreamWriter(fs); + _logWriter.AutoFlush = true; + _lock = new object(); + } + + public void WriteLine(string message) + { + lock (_lock) + { + _logWriter.WriteLine(message); + } + } + + public void WriteLine(string format, params object[] args) + { + lock (_lock) + { + _logWriter.WriteLine(format, args); + } + } + + public void Dispose() + { + lock (_lock) + { + _logWriter.Dispose(); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs b/src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs new file mode 100644 index 000000000..233372a1f --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/IProcessLogger.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.TestHelpers +{ + public enum ProcessStream + { + StandardIn = 0, + StandardOut = 1, + StandardError = 2, + MaxStreams = 3 + } + + public enum KillReason + { + TimedOut, + Unknown + } + + public interface IProcessLogger + { + void ProcessExited(ProcessRunner runner); + void ProcessKilled(ProcessRunner runner, KillReason reason); + void ProcessStarted(ProcessRunner runner); + void Write(ProcessRunner runner, string data, ProcessStream stream); + void WriteLine(ProcessRunner runner, string data, ProcessStream stream); + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs new file mode 100644 index 000000000..1a2a69519 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/IndentedTestOutputHelper.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// An implementation of ITestOutputHelper that adds one indent level to + /// the start of each line + /// + public class IndentedTestOutputHelper : ITestOutputHelper + { + readonly string _indentText; + readonly ITestOutputHelper _output; + + public IndentedTestOutputHelper(ITestOutputHelper innerOutput, string indentText = " ") + { + _output = innerOutput; + _indentText = indentText; + } + + public void WriteLine(string message) + { + _output.WriteLine(_indentText + message); + } + + public void WriteLine(string format, params object[] args) + { + _output.WriteLine(_indentText + format, args); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj b/src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj new file mode 100644 index 000000000..0800c15f0 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Microsoft.Diagnostics.TestHelpers.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.0 + true + ;1591;1701 + true + Diagnostic test support + $(Description) + tests + embedded + + + + + + + diff --git a/src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs b/src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs new file mode 100644 index 000000000..6814a1003 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/MultiplexTestOutputHelper.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public class MultiplexTestOutputHelper : ITestOutputHelper + { + readonly ITestOutputHelper[] _outputs; + + public MultiplexTestOutputHelper(params ITestOutputHelper[] outputs) + { + _outputs = outputs; + } + + public void WriteLine(string message) + { + foreach(ITestOutputHelper output in _outputs) + { + output.WriteLine(message); + } + } + + public void WriteLine(string format, params object[] args) + { + foreach (ITestOutputHelper output in _outputs) + { + output.WriteLine(format, args); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs b/src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs new file mode 100644 index 000000000..0c6cb51f0 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/PrebuiltDebuggeeCompiler.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public class PrebuiltDebuggeeCompiler : IDebuggeeCompiler + { + string _sourcePath; + string _binaryPath; + string _binaryExePath; + + public PrebuiltDebuggeeCompiler(TestConfiguration config, string debuggeeName) + { + //we anticipate paths like this: + //Source: //[] + //Binaries: // + _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 Execute(ITestOutputHelper output) + { + return Task.Factory.StartNew(() => new DebuggeeConfiguration(_sourcePath, _binaryPath, _binaryExePath)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs b/src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs new file mode 100644 index 000000000..5dad0e5d0 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/ProcessRunner.cs @@ -0,0 +1,469 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// Executes a process and logs the output + /// + /// + /// 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. + /// + 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 _loggers; + Process _p; + DateTime _startTime; + TimeSpan _timeout; + ITestOutputHelper _traceOutput; + int? _expectedExitCode; + TaskCompletionSource _waitForProcessStartTaskSource; + Task _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(); + _timeout = TimeSpan.FromMinutes(10); + _cancelSource = new CancellationTokenSource(); + _killReason = null; + _waitForProcessStartTaskSource = new TaskCompletionSource(); + Task 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 EnvironmentVariables + { + get { lock (_lock) { return new Dictionary(_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 Run() + { + Start(); + return WaitForExit(); + } + + public Task 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 InternalWaitForExit(Task startProcessTask, Task stdOutTask, Task stdErrTask) + { + DebugTrace("starting InternalWaitForExit"); + Process p = await startProcessTask; + DebugTrace("InternalWaitForExit {0} '{1}'", p.Id, _replayCommand); + + Task processExit = Task.Factory.StartNew(() => + { + DebugTrace("starting Process.WaitForExit {0}", p.Id); + p.WaitForExit(); + DebugTrace("ending Process.WaitForExit {0}", p.Id); + }, + TaskCreationOptions.LongRunning); + + DebugTrace("awaiting process {0} exit, stdOut, and stdErr", p.Id); + await Task.WhenAll(processExit, stdOutTask, stdErrTask); + DebugTrace("await process {0} exit, stdOut, and stdErr complete", p.Id); + + foreach (IProcessLogger logger in Loggers) + { + logger.ProcessExited(this); + } + + lock (_lock) + { + if (_expectedExitCode.HasValue && p.ExitCode != _expectedExitCode.Value) + { + throw new Exception("Process returned exit code " + p.ExitCode + ", expected " + _expectedExitCode.Value + Environment.NewLine + + "Command Line: " + ReplayCommand + Environment.NewLine + + "Working Directory: " + WorkingDirectory); + } + DebugTrace("InternalWaitForExit {0} returning {1}", p.Id, p.ExitCode); + return p.ExitCode; + } + } + + private void DebugTrace(string format, params object[] args) + { + lock (_lock) + { + if (_traceOutput != null) + { + string message = string.Format(format, args); + _traceOutput.WriteLine("TRACE: {0}", message); + } + } + } + + class ConsoleTestOutputHelper : ITestOutputHelper + { + readonly ITestOutputHelper _output; + + public ConsoleTestOutputHelper(ITestOutputHelper output) + { + _output = output; + } + + public void WriteLine(string message) + { + Console.WriteLine(message); + if (_output != null) + { + _output.WriteLine(message); + } + + } + + public void WriteLine(string format, params object[] args) + { + Console.WriteLine(format, args); + if (_output != null) + { + _output.WriteLine(format, args); + } + } + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs b/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs new file mode 100644 index 000000000..98c16a333 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/TestConfiguration.cs @@ -0,0 +1,702 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml.Linq; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// Represents the all the test configurations for a test run. + /// + public class TestRunConfiguration : IDisposable + { + public static TestRunConfiguration Instance + { + get { return _instance.Value; } + } + + static Lazy _instance = new Lazy(() => 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 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 initialConfig = new Dictionary + { + ["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> configs = ParseConfigFile(path, new Dictionary[] { initialConfig }); + Configurations = configs.Select(c => new TestConfiguration(c)); + } + + Dictionary[] ParseConfigFile(string path, Dictionary[] 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[] ParseConfigSettings(Dictionary[] templates, XElement node) + { + Dictionary[] currentTemplates = templates; + foreach (XElement child in node.Elements()) + { + currentTemplates = ParseConfigSetting(currentTemplates, child); + } + return currentTemplates; + } + + Dictionary[] ParseConfigSetting(Dictionary[] 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 currentTemplate = templates.Last(); + + switch (node.Name.LocalName) + { + case "Options": + if (EvaluateConditional(currentTemplate, node)) + { + List> newTemplates = new List>(); + foreach (XElement optionNode in node.Elements("Option")) + { + if (EvaluateConditional(currentTemplate, optionNode)) + { + IEnumerable> templateCopy = templates.Select(c => new Dictionary(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 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 config, XElement node) + { + foreach (XAttribute attr in node.Attributes("Condition")) + { + string conditionText = attr.Value; + + // Check if Exists('') + 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 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() + { + } + } + + /// + /// Represents the current test configuration + /// + public class TestConfiguration + { + const string DebugTypeKey = "DebugType"; + const string DebuggeeBuildRootKey = "DebuggeeBuildRoot"; + + internal static readonly string BaseDir = Path.GetFullPath("."); + + private Dictionary _settings; + + public TestConfiguration() + { + _settings = new Dictionary(); + } + + public TestConfiguration(Dictionary initialSettings) + { + _settings = new Dictionary(initialSettings); + } + + public IReadOnlyDictionary AllSettings + { + get { return _settings; } + } + + public TestConfiguration CloneWithNewDebugType(string pdbType) + { + Debug.Assert(!string.IsNullOrWhiteSpace(pdbType)); + + var currentSettings = new Dictionary(_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); + } + + /// + /// 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. + /// + public string TargetArchitecture + { + get { return GetValue("TargetArchitecture").ToLowerInvariant(); } + } + + /// + /// Built for "Debug" or "Release". Can be null. + /// + public string TargetConfiguration + { + get { return GetValue("TargetConfiguration"); } + } + + /// + /// The product "projectk" (.NET Core) or "desktop". + /// + public string TestProduct + { + get { return GetValue("TestProduct").ToLowerInvariant(); } + } + + /// + /// Returns true if running on .NET Core (based on TestProduct). + /// + public bool IsNETCore + { + get { return TestProduct.Equals("projectk"); } + } + + /// + /// Returns true if running on desktop framework (based on TestProduct). + /// + public bool IsDesktop + { + get { return TestProduct.Equals("desktop"); } + } + + /// + /// The test runner script directory + /// + public string ScriptRootDir + { + get { return MakeCanonicalPath(GetValue("ScriptRootDir")); } + } + + /// + /// Working temporary directory. + /// + public string WorkingDir + { + get { return MakeCanonicalPath(GetValue("WorkingDir")); } + } + + /// + /// The host program to run a .NET Core or null for desktop/no host. + /// + public string HostExe + { + get { return MakeCanonicalExePath(GetValue("HostExe")); } + } + + /// + /// Arguments to the HostExe. + /// + public string HostArgs + { + get { return GetValue("HostArgs"); } + } + + /// + /// Environment variables to pass to the target process (via the ProcessRunner). + /// + public string HostEnvVars + { + get { return GetValue("HostEnvVars"); } + } + + /// + /// Add the host environment variables to the process runner. + /// + /// process runner instance + 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]); + } + } + } + + /// + /// The directory to the runtime (coreclr.dll, etc.) symbols + /// + public string RuntimeSymbolsPath + { + get { return MakeCanonicalPath(GetValue("RuntimeSymbolsPath")); } + } + + /// + /// How the debuggees are built: "prebuilt" or "cli" (builds the debuggee during the test run with build and cli configuration). + /// + public string DebuggeeBuildProcess + { + get { return GetValue("DebuggeeBuildProcess")?.ToLowerInvariant(); } + } + + /// + /// Debuggee sources and template project file will be retrieved from here: //[] + /// + public string DebuggeeSourceRoot + { + get { return MakeCanonicalPath(GetValue("DebuggeeSourceRoot")); } + } + + /// + /// Debuggee final sources/project file/binary outputs will be placed here: // + /// + public string DebuggeeBuildRoot + { + get { return MakeCanonicalPath(GetValue(DebuggeeBuildRootKey)); } + } + + /// + /// Debuggee native binary dependencies will be retrieved from here. + /// + public string DebuggeeNativeLibRoot + { + get { return MakeCanonicalPath(GetValue("DebuggeeNativeLibRoot")); } + } + + /// + /// The version of the Microsoft.NETCore.App package to reference when building the debuggee. + /// + public string BuildProjectMicrosoftNetCoreAppVersion + { + get { return GetValue("BuildProjectMicrosoftNetCoreAppVersion"); } + } + + /// + /// The framework type/version used to build the debuggee like "netcoreapp2.0" or "netstandard1.0". + /// + public string BuildProjectFramework + { + get { return GetValue("BuildProjectFramework"); } + } + + /// + /// 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. + /// + public string BuildProjectRuntime + { + get { return GetValue("BuildProjectRuntime"); } + } + + /// + /// The version of the Microsoft.NETCore.App package to reference when running the debuggee (i.e. + /// using the dotnet cli --fx-version option). + /// + public string RuntimeFrameworkVersion + { + get { return GetValue("RuntimeFrameworkVersion"); } + } + + /// + /// The major portion of the runtime framework version + /// + 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"); + } + } + + /// + /// The type of PDB: "full" (Windows PDB) or "portable". + /// + public string DebugType + { + get { return GetValue(DebugTypeKey); } + } + + /// + /// Either the local path to the dotnet cli to build or the URL of the runtime to download and install. + /// + public string CliPath + { + get { return MakeCanonicalPath(GetValue("CliPath")); } + } + + /// + /// The local path to put the downloaded and decompressed runtime. + /// + public string CliCacheRoot + { + get { return MakeCanonicalPath(GetValue("CliCacheRoot")); } + } + + /// + /// The version (i.e. 2.0.0) of the dotnet cli to use. + /// + public string CliVersion + { + get { return GetValue("CliVersion"); } + } + + /// + /// The directory to cache the nuget packages on restore + /// + public string NuGetPackageCacheDir + { + get { return MakeCanonicalPath(GetValue("NuGetPackageCacheDir")); } + } + + /// + /// The nuget package feeds separated by semicolons. + /// + public string NuGetPackageFeeds + { + get { return GetValue("NuGetPackageFeeds"); } + } + + /// + /// If true, log the test output, etc. to the console. + /// + public bool LogToConsole + { + get { return bool.TryParse(GetValue("LogToConsole"), out bool b) && b; } + } + + /// + /// The directory to put the test logs. + /// + public string LogDirPath + { + get { return MakeCanonicalPath(GetValue("LogDir")); } + } + + /// + /// The "ILLink.Tasks" package version to reference or null. + /// + public string LinkerPackageVersion + { + get { return GetValue("LinkerPackageVersion"); } + } + + #region Runtime Features properties + + /// + /// Returns true if the "createdump" facility exists. + /// + public bool CreateDumpExists + { + get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor > 1; } + } + + /// + /// Returns true if a stack overflow causes dump to be generated with createdump. 3.x has now started to + /// create dumps on stack overflow. + /// + public bool StackOverflowCreatesDump + { + get { return IsNETCore && RuntimeFrameworkVersionMajor >= 3; } + } + + /// + /// Returns true if a stack overflow causes a SIGSEGV exception instead of aborting. + /// + public bool StackOverflowSIGSEGV + { + get { return OS.Kind == OSKind.Linux && IsNETCore && RuntimeFrameworkVersionMajor == 1; } + } + + #endregion + + /// + /// Returns the configuration value for the key or null. + /// + /// name of the configuration value + /// configuration value or null + 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; + } + } + + /// + /// The OS running + /// + public enum OSKind + { + Windows, + Linux, + OSX, + Unknown, + } + + /// + /// The OS specific configuration + /// + 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; + } + } + + /// + /// The OS the tests are running. + /// + public static OSKind Kind { get; private set; } + + /// + /// 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. + /// + public static Architecture TargetArchitecture { get { return RuntimeInformation.ProcessArchitecture; } } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs b/src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs new file mode 100644 index 000000000..6966668dd --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/TestOutputProcessLogger.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public class TestOutputProcessLogger : IProcessLogger + { + string _timeFormat = "mm\\:ss\\.fff"; + ITestOutputHelper _output; + StringBuilder[] _lineBuffers; + + public TestOutputProcessLogger(ITestOutputHelper output) + { + _output = output; + _lineBuffers = new StringBuilder[(int)ProcessStream.MaxStreams]; + } + + public void ProcessStarted(ProcessRunner runner) + { + lock (this) + { + _output.WriteLine("Running Process: " + runner.ReplayCommand); + _output.WriteLine("Working Directory: " + runner.WorkingDirectory); + IEnumerable> additionalEnvVars = + runner.EnvironmentVariables.Where(kv => Environment.GetEnvironmentVariable(kv.Key) != kv.Value); + + if(additionalEnvVars.Any()) + { + _output.WriteLine("Additional Environment Variables: " + + string.Join(", ", additionalEnvVars.Select(kv => kv.Key + "=" + kv.Value))); + } + _output.WriteLine("{"); + } + } + + public virtual void Write(ProcessRunner runner, string data, ProcessStream stream) + { + lock (this) + { + AppendToLineBuffer(runner, stream, data); + } + } + + public virtual void WriteLine(ProcessRunner runner, string data, ProcessStream stream) + { + lock (this) + { + StringBuilder lineBuffer = AppendToLineBuffer(runner, stream, data); + //Ensure all output is written even if it isn't a full line before we log input + if (stream == ProcessStream.StandardIn) + { + FlushOutput(); + } + _output.WriteLine(lineBuffer.ToString()); + _lineBuffers[(int)stream] = null; + } + } + + public virtual void ProcessExited(ProcessRunner runner) + { + lock (this) + { + TimeSpan offset = runner.StartTime - DateTime.Now; + _output.WriteLine("}"); + _output.WriteLine("Exit code: " + runner.ExitCode + " ( " + offset.ToString(_timeFormat) + " elapsed)"); + _output.WriteLine(""); + } + } + + public void ProcessKilled(ProcessRunner runner, KillReason reason) + { + lock (this) + { + TimeSpan offset = runner.StartTime - DateTime.Now; + string reasonText = ""; + if (reason == KillReason.TimedOut) + { + reasonText = "Process timed out"; + } + else if (reason == KillReason.Unknown) + { + reasonText = "Kill() was called"; + } + _output.WriteLine(" Killing process: " + offset.ToString(_timeFormat) + ": " + reasonText); + } + } + + protected void FlushOutput() + { + if (_lineBuffers[(int)ProcessStream.StandardOut] != null) + { + _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardOut].ToString()); + _lineBuffers[(int)ProcessStream.StandardOut] = null; + } + if (_lineBuffers[(int)ProcessStream.StandardError] != null) + { + _output.WriteLine(_lineBuffers[(int)ProcessStream.StandardError].ToString()); + _lineBuffers[(int)ProcessStream.StandardError] = null; + } + } + + private StringBuilder AppendToLineBuffer(ProcessRunner runner, ProcessStream stream, string data) + { + StringBuilder lineBuffer = _lineBuffers[(int)stream]; + if (lineBuffer == null) + { + TimeSpan offset = runner.StartTime - DateTime.Now; + lineBuffer = new StringBuilder(); + lineBuffer.Append(" "); + if (stream == ProcessStream.StandardError) + { + lineBuffer.Append("STDERROR: "); + } + else if (stream == ProcessStream.StandardIn) + { + lineBuffer.Append("STDIN: "); + } + lineBuffer.Append(offset.ToString(_timeFormat)); + lineBuffer.Append(": "); + _lineBuffers[(int)stream] = lineBuffer; + } + + // xunit has a bug where a non-printable character isn't properly escaped when + // it is written into the xml results which ultimately results in + // the xml being improperly truncated. For example MDbg has a test case that prints + // \0 and dotnet tools print \u001B to colorize their console output. + foreach(char c in data) + { + if(!char.IsControl(c)) + { + lineBuffer.Append(c); + } + } + return lineBuffer; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs b/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs new file mode 100644 index 000000000..c12c5328f --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/TestRunner.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + public class TestRunner + { + /// + /// Run debuggee (without any debugger) and compare the console output to the regex specified. + /// + /// test config to use + /// output helper + /// test case name + /// debuggee name (no path) + /// regex to match on console (standard and error) output + /// + public static async Task 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(); + } + } + + /// + /// Returns a test config for each PDB type supported by the product/platform. + /// + /// starting config + /// new configs for each supported PDB type + public static IEnumerable 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); + } + } + } + + /// + /// Returns an output helper for the specified config. + /// + /// test config + /// starting output helper + /// test case name + /// new output helper + public static TestRunner.OutputHelper ConfigureLogging(TestConfiguration config, ITestOutputHelper output, string testName) + { + FileTestOutputHelper fileLogger = null; + ConsoleTestOutputHelper consoleLogger = null; + if (!string.IsNullOrEmpty(config.LogDirPath)) + { + string logFileName = testName + "." + config.ToString() + ".log"; + string logPath = Path.Combine(config.LogDirPath, logFileName); + fileLogger = new FileTestOutputHelper(logPath, FileMode.Append); + } + if (config.LogToConsole) + { + consoleLogger = new ConsoleTestOutputHelper(); + } + return new TestRunner.OutputHelper(output, fileLogger, consoleLogger); + } + + public class OutputHelper : ITestOutputHelper, IDisposable + { + readonly ITestOutputHelper _output; + readonly FileTestOutputHelper _fileLogger; + readonly ConsoleTestOutputHelper _consoleLogger; + + public readonly ITestOutputHelper IndentedOutput; + + public OutputHelper(ITestOutputHelper output, FileTestOutputHelper fileLogger, ConsoleTestOutputHelper consoleLogger) + { + _output = output; + _fileLogger = fileLogger; + _consoleLogger = consoleLogger; + IndentedOutput = new IndentedTestOutputHelper(this); + } + + public void WriteLine(string message) + { + _output.WriteLine(message); + _fileLogger?.WriteLine(message); + _consoleLogger?.WriteLine(message); + } + + public void WriteLine(string format, params object[] args) + { + _output.WriteLine(format, args); + _fileLogger?.WriteLine(format, args); + _consoleLogger?.WriteLine(format, args); + } + + public void Dispose() + { + _fileLogger?.Dispose(); + } + } + + public class TestLogger : TestOutputProcessLogger + { + readonly StringBuilder _standardOutput; + readonly StringBuilder _standardError; + + public TestLogger(ITestOutputHelper output) + : base(output) + { + lock (this) + { + _standardOutput = new StringBuilder(); + _standardError = new StringBuilder(); + } + } + + public string GetStandardOutput() + { + lock (this) + { + return _standardOutput.ToString(); + } + } + + public string GetStandardError() + { + lock (this) + { + return _standardError.ToString(); + } + } + + public override void Write(ProcessRunner runner, string data, ProcessStream stream) + { + lock (this) + { + base.Write(runner, data, stream); + switch (stream) + { + case ProcessStream.StandardOut: + _standardOutput.Append(data); + break; + + case ProcessStream.StandardError: + _standardError.Append(data); + break; + } + } + } + + public override void WriteLine(ProcessRunner runner, string data, ProcessStream stream) + { + lock (this) + { + base.WriteLine(runner, data, stream); + switch (stream) + { + case ProcessStream.StandardOut: + _standardOutput.AppendLine(data); + break; + + case ProcessStream.StandardError: + _standardError.AppendLine(data); + break; + } + } + } + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestStep.cs b/src/Microsoft.Diagnostics.TestHelpers/TestStep.cs new file mode 100644 index 000000000..e4c12bf4f --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/TestStep.cs @@ -0,0 +1,636 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.TestHelpers +{ + /// + /// 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 + /// + 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 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(""); + if(indexOfInitialStepStateElementEnd == -1) + { + return false; + } + int splitIndex = indexOfInitialStepStateElementEnd + "".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(runStateElement.Value, out runState)) + { + return; + } + DateTimeOffset? completeTime = null; + XElement completeTimeElement = finalTaskStateElement.Element("CompleteTime"); + if (completeTimeElement != null) + { + DateTimeOffset tempCompleteTime; + if (!DateTimeOffset.TryParse(completeTimeElement.Value, out tempCompleteTime)) + { + return; + } + else + { + completeTime = tempCompleteTime; + } + } + XElement errorMessageElement = finalTaskStateElement.Element("ErrorMessage"); + string errorMessage = null; + if (errorMessageElement != null) + { + errorMessage = errorMessageElement.Value; + } + XElement errorStackTraceElement = finalTaskStateElement.Element("ErrorStackTrace"); + string errorStackTrace = null; + if (errorStackTraceElement != null) + { + errorStackTrace = errorStackTraceElement.Value; + } + + taskState = taskState.WithFinalState(runState, completeTime, errorMessage, errorStackTrace); + } + catch (XmlException) { } + } + } + } + + public class TestStepException : Exception + { + public TestStepException(string errorMessage) : + base(errorMessage) + { } + + public TestStepException(string stepName, string errorMessage, string stackTrace) : + base("The " + stepName + " test step failed." + Environment.NewLine + + "Original Error: " + errorMessage + Environment.NewLine + + stackTrace) + { } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs new file mode 100644 index 000000000..21c5dbf6e --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkipTestException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Xunit.Extensions +{ + public class SkipTestException : Exception + { + public SkipTestException(string reason) + : base(reason) { } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs new file mode 100644 index 000000000..f3490aed5 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactAttribute.cs @@ -0,0 +1,8 @@ +using Xunit; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableFactDiscoverer", "Microsoft.Diagnostics.TestHelpers")] + public class SkippableFactAttribute : FactAttribute { } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs new file mode 100644 index 000000000..b24916055 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactDiscoverer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer + { + readonly IMessageSink diagnosticMessageSink; + + public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + } + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + yield return new SkippableFactTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs new file mode 100644 index 000000000..9a7fa3074 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactMessageBus.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + public class SkippableFactMessageBus : IMessageBus + { + readonly IMessageBus innerBus; + + public SkippableFactMessageBus(IMessageBus innerBus) + { + this.innerBus = innerBus; + } + + public int DynamicallySkippedTestCount { get; private set; } + + public void Dispose() { } + + public bool QueueMessage(IMessageSinkMessage message) + { + var testFailed = message as ITestFailed; + if (testFailed != null) + { + var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); + if (exceptionType == typeof(SkipTestException).FullName) + { + DynamicallySkippedTestCount++; + return innerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); + } + } + + // Nothing we care about, send it on its way + return innerBus.QueueMessage(message); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs new file mode 100644 index 000000000..e031171d9 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableFactTestCase.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + public class SkippableFactTestCase : XunitTestCase + { + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableFactTestCase() { } + + public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { } + + public override async Task RunAsync(IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var skipMessageBus = new SkippableFactMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource); + if (skipMessageBus.DynamicallySkippedTestCount > 0) + { + result.Failed -= skipMessageBus.DynamicallySkippedTestCount; + result.Skipped += skipMessageBus.DynamicallySkippedTestCount; + } + + return result; + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs new file mode 100644 index 000000000..c5823500b --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryAttribute.cs @@ -0,0 +1,7 @@ +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + [XunitTestCaseDiscoverer("Xunit.Extensions.SkippableTheoryDiscoverer", "Microsoft.Diagnostics.TestHelpers")] + public class SkippableTheoryAttribute : TheoryAttribute { } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs new file mode 100644 index 000000000..40c4ab7dd --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryDiscoverer.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + public class SkippableTheoryDiscoverer : IXunitTestCaseDiscoverer + { + readonly IMessageSink diagnosticMessageSink; + readonly TheoryDiscoverer theoryDiscoverer; + + public SkippableTheoryDiscoverer(IMessageSink diagnosticMessageSink) + { + this.diagnosticMessageSink = diagnosticMessageSink; + + theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink); + } + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + var defaultMethodDisplay = discoveryOptions.MethodDisplayOrDefault(); + var defaultMethodDisplayOptions = discoveryOptions.MethodDisplayOptionsOrDefault(); + + // Unlike fact discovery, the underlying algorithm for theories is complex, so we let the theory discoverer + // do its work, and do a little on-the-fly conversion into our own test cases. + return theoryDiscoverer.Discover(discoveryOptions, testMethod, factAttribute) + .Select(testCase => testCase is XunitTheoryTestCase + ? (IXunitTestCase)new SkippableTheoryTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod) + : new SkippableFactTestCase(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testCase.TestMethod, testCase.TestMethodArguments)); + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs new file mode 100644 index 000000000..46d2037ee --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/SkippableTheoryTestCase.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Xunit.Extensions +{ + public class SkippableTheoryTestCase : XunitTheoryTestCase + { + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTheoryTestCase() { } + + public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } + + public override async Task RunAsync(IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + // Duplicated code from SkippableFactTestCase. I'm sure we could find a way to de-dup with some thought. + var skipMessageBus = new SkippableFactMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource); + if (skipMessageBus.DynamicallySkippedTestCount > 0) + { + result.Failed -= skipMessageBus.DynamicallySkippedTestCount; + result.Skipped += skipMessageBus.DynamicallySkippedTestCount; + } + + return result; + } + } +} diff --git a/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt new file mode 100644 index 000000000..39b2d6592 --- /dev/null +++ b/src/Microsoft.Diagnostics.TestHelpers/Xunit.Extensions/license.txt @@ -0,0 +1,15 @@ +Source in this directory is derived source at https://github.com/xunit/samples.xunit. The repo provided the following license: + +Copyright 2014 Outercurve Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj b/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj index bf38f5910..828eaab8b 100644 --- a/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj +++ b/src/SOS/SOS.UnitTests/SOS.UnitTests.csproj @@ -27,13 +27,13 @@ Always - - - - + + + + diff --git a/src/SOS/SOS.UnitTests/SOS.cs b/src/SOS/SOS.UnitTests/SOS.cs index 16f439203..8db30a6c6 100644 --- a/src/SOS/SOS.UnitTests/SOS.cs +++ b/src/SOS/SOS.UnitTests/SOS.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostic.TestHelpers; +using Microsoft.Diagnostics.TestHelpers; using System; using System.Collections.Generic; using System.IO; diff --git a/src/SOS/SOS.UnitTests/SOSRunner.cs b/src/SOS/SOS.UnitTests/SOSRunner.cs index 839df9d59..11a81b410 100644 --- a/src/SOS/SOS.UnitTests/SOSRunner.cs +++ b/src/SOS/SOS.UnitTests/SOSRunner.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostic.TestHelpers; +using Microsoft.Diagnostics.TestHelpers; using System; using System.Collections.Generic; using System.IO;