Remove StreamForwarder from CLI tests (#4936)
* WiP * Replace StreamForwarder with OutputDataReceived * Add build logging around test execution * add newlines * Add handling for the null terminator while maintaining empty lines * Extra Diag * Verbose logging for VS Test Console * Fix asset file locking tests * Add testcommand timeout + improve dotnet-new tests * WiP * Welcome, JoSequ! * Fix failing tests * Clean out diagnostics writelines * self-PR1
This commit is contained in:
parent
2b7e9b6524
commit
2fbafe6f3f
21 changed files with 353 additions and 753 deletions
|
@ -8,91 +8,34 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Test.Utilities
|
||||
{
|
||||
public class TestCommand
|
||||
{
|
||||
protected string _command;
|
||||
|
||||
private string _baseDirectory;
|
||||
|
||||
public string WorkingDirectory { get; set; }
|
||||
private List<string> _cliGeneratedEnvironmentVariables = new List<string> { "MSBuildSDKsPath" };
|
||||
|
||||
public Process CurrentProcess { get; set; }
|
||||
protected string _command;
|
||||
|
||||
public Process CurrentProcess { get; private set; }
|
||||
|
||||
public Dictionary<string, string> Environment { get; } = new Dictionary<string, string>();
|
||||
|
||||
private List<Action<string>> _writeLines = new List<Action<string>>();
|
||||
public event DataReceivedEventHandler ErrorDataReceived;
|
||||
|
||||
private List<string> _cliGeneratedEnvironmentVariables = new List<string> { "MSBuildSDKsPath" };
|
||||
public event DataReceivedEventHandler OutputDataReceived;
|
||||
|
||||
public string WorkingDirectory { get; set; }
|
||||
|
||||
public TestCommand(string command)
|
||||
{
|
||||
_command = command;
|
||||
#if NET451
|
||||
_baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
#else
|
||||
_baseDirectory = AppContext.BaseDirectory;
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual CommandResult Execute(string args = "")
|
||||
{
|
||||
var commandPath = _command;
|
||||
ResolveCommand(ref commandPath, ref args);
|
||||
|
||||
Console.WriteLine($"Executing - {commandPath} {args} - {WorkingDirectoryInfo()}");
|
||||
|
||||
var stdOut = new StreamForwarder();
|
||||
var stdErr = new StreamForwarder();
|
||||
|
||||
AddWriteLine(Reporter.Output.WriteLine);
|
||||
|
||||
stdOut.ForwardTo(writeLine: WriteLine);
|
||||
stdErr.ForwardTo(writeLine: WriteLine);
|
||||
|
||||
return RunProcess(commandPath, args, stdOut, stdErr);
|
||||
}
|
||||
|
||||
public virtual Task<CommandResult> ExecuteAsync(string args = "")
|
||||
{
|
||||
var commandPath = _command;
|
||||
ResolveCommand(ref commandPath, ref args);
|
||||
|
||||
Console.WriteLine($"Executing - {commandPath} {args} - {WorkingDirectoryInfo()}");
|
||||
|
||||
var stdOut = new StreamForwarder();
|
||||
var stdErr = new StreamForwarder();
|
||||
|
||||
AddWriteLine(Reporter.Output.WriteLine);
|
||||
|
||||
stdOut.ForwardTo(writeLine: WriteLine);
|
||||
stdErr.ForwardTo(writeLine: WriteLine);
|
||||
|
||||
return RunProcessAsync(commandPath, args, stdOut, stdErr);
|
||||
}
|
||||
|
||||
public virtual CommandResult ExecuteWithCapturedOutput(string args = "")
|
||||
{
|
||||
var command = _command;
|
||||
ResolveCommand(ref command, ref args);
|
||||
var commandPath = Env.GetCommandPath(command, ".exe", ".cmd", "") ??
|
||||
Env.GetCommandPathFromRootPath(_baseDirectory, command, ".exe", ".cmd", "");
|
||||
|
||||
Console.WriteLine($"Executing (Captured Output) - {commandPath} {args} - {WorkingDirectoryInfo()}");
|
||||
|
||||
var stdOut = new StreamForwarder();
|
||||
var stdErr = new StreamForwarder();
|
||||
|
||||
stdOut.ForwardTo(writeLine: WriteLine);
|
||||
stdErr.ForwardTo(writeLine: WriteLine);
|
||||
|
||||
stdOut.Capture();
|
||||
stdErr.Capture();
|
||||
|
||||
return RunProcess(commandPath, args, stdOut, stdErr);
|
||||
_baseDirectory = GetBaseDirectory();
|
||||
}
|
||||
|
||||
public void KillTree()
|
||||
|
@ -105,176 +48,88 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
CurrentProcess.KillTree();
|
||||
}
|
||||
|
||||
public void AddWriteLine(Action<string> writeLine)
|
||||
public virtual CommandResult Execute(string args = "")
|
||||
{
|
||||
_writeLines.Add(writeLine);
|
||||
return Task.Run(async () => await ExecuteAsync(args)).Result;
|
||||
}
|
||||
|
||||
private void ResolveCommand(ref string executable, ref string args)
|
||||
public async virtual Task<CommandResult> ExecuteAsync(string args = "")
|
||||
{
|
||||
if (executable.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var newArgs = ArgumentEscaper.EscapeSingleArg(executable);
|
||||
if (!string.IsNullOrEmpty(args))
|
||||
{
|
||||
newArgs += " " + args;
|
||||
}
|
||||
args = newArgs;
|
||||
executable = "dotnet";
|
||||
}
|
||||
var resolvedCommand = _command;
|
||||
|
||||
if (!Path.IsPathRooted(executable))
|
||||
{
|
||||
executable = Env.GetCommandPath(executable) ??
|
||||
Env.GetCommandPathFromRootPath(_baseDirectory, executable);
|
||||
}
|
||||
ResolveCommand(ref resolvedCommand, ref args);
|
||||
|
||||
Console.WriteLine($"Executing - {resolvedCommand} {args} - {WorkingDirectoryInfo()}");
|
||||
|
||||
return await ExecuteAsyncInternal(resolvedCommand, args);
|
||||
}
|
||||
|
||||
private CommandResult RunProcess(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
|
||||
public virtual CommandResult ExecuteWithCapturedOutput(string args = "")
|
||||
{
|
||||
Task taskOut = null;
|
||||
var resolvedCommand = _command;
|
||||
|
||||
Task taskErr = null;
|
||||
|
||||
CurrentProcess = CreateProcess(executable, args);
|
||||
ResolveCommand(ref resolvedCommand, ref args);
|
||||
|
||||
CurrentProcess.Start();
|
||||
var commandPath = Env.GetCommandPath(resolvedCommand, ".exe", ".cmd", "") ??
|
||||
Env.GetCommandPathFromRootPath(_baseDirectory, resolvedCommand, ".exe", ".cmd", "");
|
||||
|
||||
try
|
||||
Console.WriteLine($"Executing (Captured Output) - {commandPath} {args} - {WorkingDirectoryInfo()}");
|
||||
|
||||
return Task.Run(async () => await ExecuteAsyncInternal(resolvedCommand, args)).Result;
|
||||
}
|
||||
|
||||
|
||||
private async Task<CommandResult> ExecuteAsyncInternal(string executable, string args)
|
||||
{
|
||||
var stdOut = new List<String>();
|
||||
|
||||
var stdErr = new List<String>();
|
||||
|
||||
CurrentProcess = CreateProcess(executable, args);
|
||||
|
||||
CurrentProcess.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
|
||||
}
|
||||
catch (System.InvalidOperationException e)
|
||||
{
|
||||
if (!e.Message.Equals("The collection has been marked as complete with regards to additions."))
|
||||
stdErr.Add(e.Data);
|
||||
|
||||
var handler = ErrorDataReceived;
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
throw;
|
||||
handler(s, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
CurrentProcess.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
taskErr = stdErr.BeginRead(CurrentProcess.StandardError);
|
||||
}
|
||||
catch (System.InvalidOperationException e)
|
||||
{
|
||||
if (!e.Message.Equals("The collection has been marked as complete with regards to additions."))
|
||||
stdOut.Add(e.Data);
|
||||
|
||||
var handler = OutputDataReceived;
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
throw;
|
||||
handler(s, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var completionTask = CurrentProcess.StartAndWaitForExitAsync();
|
||||
|
||||
CurrentProcess.BeginOutputReadLine();
|
||||
|
||||
CurrentProcess.BeginErrorReadLine();
|
||||
|
||||
await completionTask;
|
||||
|
||||
CurrentProcess.WaitForExit();
|
||||
|
||||
var tasksToAwait = new List<Task>();
|
||||
RemoveNullTerminator(stdOut);
|
||||
|
||||
if (taskOut != null)
|
||||
{
|
||||
tasksToAwait.Add(taskOut);
|
||||
}
|
||||
RemoveNullTerminator(stdErr);
|
||||
|
||||
if (taskErr != null)
|
||||
{
|
||||
tasksToAwait.Add(taskErr);
|
||||
}
|
||||
|
||||
if (tasksToAwait.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.WaitAll(tasksToAwait.ToArray());
|
||||
}
|
||||
catch (System.ObjectDisposedException e)
|
||||
{
|
||||
taskErr = null;
|
||||
|
||||
taskOut = null;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new CommandResult(
|
||||
return new CommandResult(
|
||||
CurrentProcess.StartInfo,
|
||||
CurrentProcess.ExitCode,
|
||||
stdOut?.CapturedOutput ?? CurrentProcess.StandardOutput.ReadToEnd(),
|
||||
stdErr?.CapturedOutput ?? CurrentProcess.StandardError.ReadToEnd());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Task<CommandResult> RunProcessAsync(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr)
|
||||
{
|
||||
Task taskOut = null;
|
||||
|
||||
Task taskErr = null;
|
||||
|
||||
CurrentProcess = CreateProcess(executable, args);
|
||||
|
||||
CurrentProcess.Start();
|
||||
|
||||
try
|
||||
{
|
||||
taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput);
|
||||
}
|
||||
catch (System.InvalidOperationException e)
|
||||
{
|
||||
if (!e.Message.Equals("The collection has been marked as complete with regards to additions."))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
taskErr = stdErr.BeginRead(CurrentProcess.StandardError);
|
||||
}
|
||||
catch (System.InvalidOperationException e)
|
||||
{
|
||||
if (!e.Message.Equals("The collection has been marked as complete with regards to additions."))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<CommandResult>();
|
||||
|
||||
CurrentProcess.Exited += (sender, arg) =>
|
||||
{
|
||||
var tasksToAwait = new List<Task>();
|
||||
|
||||
if (taskOut != null)
|
||||
{
|
||||
tasksToAwait.Add(taskOut);
|
||||
}
|
||||
|
||||
if (taskErr != null)
|
||||
{
|
||||
tasksToAwait.Add(taskErr);
|
||||
}
|
||||
|
||||
if (tasksToAwait.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.WaitAll(tasksToAwait.ToArray());
|
||||
}
|
||||
catch (System.ObjectDisposedException e)
|
||||
{
|
||||
taskErr = null;
|
||||
|
||||
taskOut = null;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new CommandResult(
|
||||
CurrentProcess.StartInfo,
|
||||
CurrentProcess.ExitCode,
|
||||
stdOut?.CapturedOutput ?? CurrentProcess.StandardOutput.ReadToEnd(),
|
||||
stdErr?.CapturedOutput ?? CurrentProcess.StandardError.ReadToEnd());
|
||||
|
||||
tcs.SetResult(result);
|
||||
};
|
||||
|
||||
return tcs.Task;
|
||||
String.Join(System.Environment.NewLine, stdOut),
|
||||
String.Join(System.Environment.NewLine, stdErr));
|
||||
}
|
||||
|
||||
private Process CreateProcess(string executable, string args)
|
||||
|
@ -289,21 +144,11 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
UseShellExecute = false
|
||||
};
|
||||
|
||||
RemoveCliGeneratedEnvironmentVariables(psi);
|
||||
RemoveCliGeneratedEnvironmentVariablesFrom(psi);
|
||||
|
||||
foreach (var item in Environment)
|
||||
{
|
||||
#if NET451
|
||||
psi.EnvironmentVariables[item.Key] = item.Value;
|
||||
#else
|
||||
psi.Environment[item.Key] = item.Value;
|
||||
#endif
|
||||
}
|
||||
AddEnvironmentVariablesTo(psi);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(WorkingDirectory))
|
||||
{
|
||||
psi.WorkingDirectory = WorkingDirectory;
|
||||
}
|
||||
AddWorkingDirectoryTo(psi);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
|
@ -315,14 +160,6 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
return process;
|
||||
}
|
||||
|
||||
private void WriteLine(string line)
|
||||
{
|
||||
foreach (var writeLine in _writeLines)
|
||||
{
|
||||
writeLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private string WorkingDirectoryInfo()
|
||||
{
|
||||
if (WorkingDirectory == null)
|
||||
|
@ -333,7 +170,54 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
return $" in pwd {WorkingDirectory}";
|
||||
}
|
||||
|
||||
private void RemoveCliGeneratedEnvironmentVariables(ProcessStartInfo psi)
|
||||
private void RemoveNullTerminator(List<string> strings)
|
||||
{
|
||||
var count = strings.Count;
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (strings[count - 1] == null)
|
||||
{
|
||||
strings.RemoveAt(count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetBaseDirectory()
|
||||
{
|
||||
#if NET451
|
||||
return AppDomain.CurrentDomain.BaseDirectory;
|
||||
#else
|
||||
return AppContext.BaseDirectory;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ResolveCommand(ref string executable, ref string args)
|
||||
{
|
||||
if (executable.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var newArgs = ArgumentEscaper.EscapeSingleArg(executable);
|
||||
|
||||
if (!string.IsNullOrEmpty(args))
|
||||
{
|
||||
newArgs += " " + args;
|
||||
}
|
||||
|
||||
args = newArgs;
|
||||
|
||||
executable = "dotnet";
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(executable))
|
||||
{
|
||||
executable = Env.GetCommandPath(executable) ??
|
||||
Env.GetCommandPathFromRootPath(_baseDirectory, executable);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCliGeneratedEnvironmentVariablesFrom(ProcessStartInfo psi)
|
||||
{
|
||||
foreach (var name in _cliGeneratedEnvironmentVariables)
|
||||
{
|
||||
|
@ -344,5 +228,25 @@ namespace Microsoft.DotNet.Tools.Test.Utilities
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEnvironmentVariablesTo(ProcessStartInfo psi)
|
||||
{
|
||||
foreach (var item in Environment)
|
||||
{
|
||||
#if NET451
|
||||
psi.EnvironmentVariables[item.Key] = item.Value;
|
||||
#else
|
||||
psi.Environment[item.Key] = item.Value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWorkingDirectoryTo(ProcessStartInfo psi)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(WorkingDirectory))
|
||||
{
|
||||
psi.WorkingDirectory = WorkingDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue