//we anticipate source paths like this: //InitialSource: <DebuggeeSourceRoot>/<DebuggeeName> //DebuggeeNativeLibDir: <DebuggeeNativeLibRoot>/<DebuggeeName> //DotNetRootBuildDir: <DebuggeeBuildRoot> //DebuggeeSolutionDir: <DebuggeeBuildRoot>/<DebuggeeName> //DebuggeeProjectDir: <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>] //DebuggeeBinaryDir: <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>] //DebuggeeBinaryDll: <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/<DebuggeeName>.dll //DebuggeeBinaryExe: <DebuggeeBuildRoot>/<DebuggeeName>[/<DebuggeeName>]/bin/Debug/<framework>/[<runtime>]/<DebuggeeName>.exe //LogPath: <DebuggeeBuildRoot>/<DebuggeeName>.txt // When the runtime directory is present it will have a native host exe in it that has been renamed to the debugee // name. It also has a managed dll in it which functions as a managed exe when renamed. // When the runtime directory is missing, the framework directory will have a managed dll in it that functions if it // is renamed to an exe. I'm sure that renaming isn't the intended usage, but it works and produces less churn // in our tests for the moment. public abstract DotNetBuildDebuggeeTestStep ConfigureDotNetBuildDebuggeeTask(TestConfiguration config, string dotNetPath, string cliToolsVersion, string debuggeeName);
/// <summary> /// Creates a new BaseDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build debuggees via dotnet build. /// </summary> /// <param name="config"> /// The test configuration that will be used to configure the build. The following configuration options should be set in the config: /// CliPath The location to get the CLI tools from, either as a .zip/.tar.gz at a web endpoint, a .zip/.tar.gz /// at a local filesystem path, or the dotnet binary at a local filesystem path /// WorkingDir Temporary storage for CLI tools compressed archives downloaded from the internet will be stored here /// CliCacheRoot The final CLI tools will be expanded and cached here /// DebuggeeSourceRoot Debuggee sources and template project file will be retrieved from here /// DebuggeeNativeLibRoot Debuggee native binary dependencies will be retrieved from here /// DebuggeeBuildRoot Debuggee final sources/project file/binary outputs will be placed here /// BuildProjectRuntime The runtime moniker to be built /// BuildProjectMicrosoftNETCoreAppVersion The nuget package version of Microsoft.NETCore.App package to build against for debuggees that references this library /// NugetPackageCacheDir The directory where NuGet packages are cached during restore /// NugetFeeds The set of nuget feeds that are used to search for packages during restore /// </param> /// <param name="debuggeeName"> /// The name of the debuggee to be built, from which various build file paths are constructed. Before build it is assumed that: /// Debuggee sources are located at config.DebuggeeSourceRoot/debuggeeName/ /// Debuggee native dependencies are located at config.DebuggeeNativeLibRoot/debuggeeName/ /// /// After the build: /// Debuggee build outputs will be created at config.DebuggeeNativeLibRoot/debuggeeName/ /// A log of the build is stored at config.DebuggeeNativeLibRoot/debuggeeName.txt /// </param> public BaseDebuggeeCompiler(TestConfiguration config, string debuggeeName) { _acquireTask = ConfigureAcquireDotNetTask(config); _buildDebuggeeTask = ConfigureDotNetBuildDebuggeeTask(config, _acquireTask.LocalDotNetPath, config.CliVersion, debuggeeName); }
protected static string GetRuntime(TestConfiguration config) { return(config.BuildProjectRuntime); }
protected abstract string GetFramework(TestConfiguration config);
protected static string GetDebuggeeBinaryExePath(TestConfiguration config, string debuggeeBinaryDirPath, string debuggeeName) { return(config.IsDesktop || config.PublishSingleFile ? Path.Combine(debuggeeBinaryDirPath, debuggeeName + (OS.Kind == OSKind.Windows ? ".exe" : "")) : null); }
protected static string GetLogPath(TestConfiguration config, string framework, string runtime, string debuggeeName) { return(Path.Combine(GetDotNetRootBuildDirPath(config), $"{framework}-{runtime ?? "any"}-{debuggeeName}.txt")); }
protected static string GetDotNetRootBuildDirPath(TestConfiguration config) { return(config.DebuggeeBuildRoot); }
protected static string GetDebuggeeBinaryDllPath(TestConfiguration config, string debuggeeBinaryDirPath, string debuggeeName) { return(config.IsNETCore ? Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".dll") : null); }
protected override string GetFramework(TestConfiguration config) { return(config.BuildProjectFramework ?? "netcoreapp3.1"); }
protected static string GetInitialSourceDirPath(TestConfiguration config, string debuggeeName) { return(Path.Combine(config.DebuggeeSourceRoot, debuggeeName)); }
/// <summary> /// Creates a new CliDebuggeeCompiler. This compiler acquires the CLI tools and uses them to build and optionally link debuggees via dotnet publish. /// <param name="config"> /// LinkerPackageVersion If set, this version of the linker package will be used to link the debuggee during publish. /// </param> /// </summary> public CliDebuggeeCompiler(TestConfiguration config, string debuggeeName) : base(config, debuggeeName) { }
protected static string GetDebuggeeBinaryExePath(TestConfiguration config, string debuggeeBinaryDirPath, string debuggeeName) { return(config.IsDesktop ? Path.Combine(debuggeeBinaryDirPath, debuggeeName + ".exe") : null); }
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")); }
/// <summary> /// Run debuggee (without any debugger) and compare the console output to the regex specified. /// </summary> /// <param name="config">test config to use</param> /// <param name="output">output helper</param> /// <param name="testName">test case name</param> /// <param name="debuggeeName">debuggee name (no path)</param> /// <param name="outputRegex">regex to match on console (standard and error) output</param> /// <returns></returns> public static async Task <int> Run(TestConfiguration config, ITestOutputHelper output, string testName, string debuggeeName, string outputRegex) { OutputHelper outputHelper = null; try { // Setup the logging from the options in the config file outputHelper = ConfigureLogging(config, output, testName); // Restore and build the debuggee. The debuggee name is lower cased because the // source directory name has been lowercased by the build system. DebuggeeConfiguration debuggeeConfig = await DebuggeeCompiler.Execute(config, debuggeeName.ToLowerInvariant(), outputHelper); outputHelper.WriteLine("Starting {0}", testName); outputHelper.WriteLine("{"); // Get the full debuggee launch command line (includes the host if required) string exePath = debuggeeConfig.BinaryExePath; string arguments = debuggeeConfig.BinaryDirPath; if (!string.IsNullOrWhiteSpace(config.HostExe)) { exePath = config.HostExe; arguments = Environment.ExpandEnvironmentVariables(string.Format("{0} {1} {2}", config.HostArgs, debuggeeConfig.BinaryExePath, debuggeeConfig.BinaryDirPath)); } TestLogger testLogger = new TestLogger(outputHelper.IndentedOutput); ProcessRunner processRunner = new ProcessRunner(exePath, arguments). WithLog(testLogger). WithTimeout(TimeSpan.FromMinutes(5)); processRunner.Start(); // Wait for the debuggee to finish before getting the debuggee output int exitCode = await processRunner.WaitForExit(); string debuggeeStandardOutput = testLogger.GetStandardOutput(); string debuggeeStandardError = testLogger.GetStandardError(); // The debuggee output is all the stdout first and then all the stderr output last string debuggeeOutput = debuggeeStandardOutput + debuggeeStandardError; if (string.IsNullOrEmpty(debuggeeOutput)) { throw new Exception("No debuggee output"); } // Remove any CR's in the match string because this assembly is built on Windows (with CRs) and // ran on Linux/OS X (without CRs). outputRegex = outputRegex.Replace("\r", ""); // Now match the debuggee output and regex match string if (!new Regex(outputRegex, RegexOptions.Multiline).IsMatch(debuggeeOutput)) { throw new Exception(string.Format("\nDebuggee output:\n\n'{0}'\n\nDid not match the expression:\n\n'{1}'", debuggeeOutput, outputRegex)); } return(exitCode); } catch (Exception ex) { // Log the exception outputHelper?.WriteLine(ex.ToString()); throw; } finally { outputHelper?.WriteLine("}"); outputHelper?.Dispose(); } }
public static async Task <int> RemoteInvoke(ITestOutputHelper output, TestConfiguration config, TimeSpan timeout, string dumpPath, Func <string, Task <int> > method) { RemoteInvokeOptions options = new() { StartInfo = new ProcessStartInfo() { RedirectStandardOutput = true, RedirectStandardError = true } }; // The remoteInvokeHandle is NOT disposed (i.e. with a using) here because the RemoteExecutor dispose code uses an older (1.x) version // of clrmd that conflicts with the 2.0 version diagnostics is using and throws the exception: // // "Method not found: 'Microsoft.Diagnostics.Runtime.DataTarget Microsoft.Diagnostics.Runtime.DataTarget.AttachToProcess(Int32, UInt32)'." // // When RemoteExecutor is fixed the "using" can be added and the GC.SuppressFinalize be removed. RemoteInvokeHandle remoteInvokeHandle = RemoteExecutor.Invoke(method, config.Serialize(), options); GC.SuppressFinalize(remoteInvokeHandle); try { Task stdOutputTask = WriteStreamToOutput(remoteInvokeHandle.Process.StandardOutput, output); Task stdErrorTask = WriteStreamToOutput(remoteInvokeHandle.Process.StandardError, output); Task outputTasks = Task.WhenAll(stdErrorTask, stdOutputTask); Task processExit = Task.Factory.StartNew(() => remoteInvokeHandle.Process.WaitForExit(), TaskCreationOptions.LongRunning); Task timeoutTask = Task.Delay(timeout); Task completedTask = await Task.WhenAny(outputTasks, processExit, timeoutTask); if (completedTask == timeoutTask) { if (!string.IsNullOrEmpty(dumpPath)) { output.WriteLine($"RemoteExecutorHelper.RemoteInvoke timed out: writing dump to {dumpPath}"); DiagnosticsClient client = new(remoteInvokeHandle.Process.Id); try { await client.WriteDumpAsync(DumpType.WithHeap, dumpPath, WriteDumpFlags.None, CancellationToken.None); } catch (Exception ex) when(ex is ArgumentException || ex is UnsupportedCommandException || ex is ServerErrorException) { output.WriteLine($"RemoteExecutorHelper.RemoteInvoke: writing dump FAILED {ex}"); } } throw new XunitException("RemoteExecutorHelper.RemoteInvoke timed out"); } else { return(remoteInvokeHandle.ExitCode); } } finally { if (remoteInvokeHandle.Process != null) { try { output.WriteLine($"RemoteExecutorHelper.RemoteInvoke: killing process {remoteInvokeHandle.Process.Id}"); remoteInvokeHandle.Process.Kill(entireProcessTree: true); } catch { } remoteInvokeHandle.Process.Dispose(); remoteInvokeHandle.Process = null; } } }