Skip to content

Commit

Permalink
Refactor test process manager to capture output streams
Browse files Browse the repository at this point in the history
  • Loading branch information
menees committed Oct 16, 2024
1 parent 918233d commit bc54156
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 70 deletions.
7 changes: 7 additions & 0 deletions tests/Menees.Remoting.TestHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public static int Main(string[] args)
{
ExitCode exitCode = ExitCode.Default;

WriteLine($"Arg count: {args.Length}");
foreach (string arg in args)
{
WriteLine(arg);
}

const int RequiredArgCount = 5;

// Sometimes a lighter weight option is to use SysInternals PipeList utility from PowerShell
Expand Down Expand Up @@ -112,6 +118,7 @@ public static int Main(string[] args)
}
}

WriteLine($"Exit code: {exitCode}");
return (int)exitCode;
}

Expand Down
89 changes: 19 additions & 70 deletions tests/Menees.Remoting.Tests/BaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,15 @@ protected static Task TestCrossProcessClientAsync(int clientCount, string server
new ParallelOptions { MaxDegreeOfParallelism = Math.Min(clientCount, 8 * Environment.ProcessorCount) },
item =>
{
InitializeProcessStartInfo(typeof(TestClient.Program), out ProcessStartInfo startInfo, out List<object> arguments);
arguments.Add(scenario);
arguments.Add(serverPathPrefix);
arguments.Add(timeout);
arguments.Add(iterations);
FinalizeProcessStartInfo(startInfo, arguments);

using Process clientProcess = new();
clientProcess.StartInfo = startInfo;
clientProcess.Start().ShouldBeTrue();
ProcessManager processManager = new(typeof(TestClient.Program));
processManager.Add(scenario);
processManager.Add(serverPathPrefix);
processManager.Add(timeout);
processManager.Add(iterations);

using Process clientProcess = processManager.Start();
TimeSpan exitWait = TimeSpan.FromSeconds(30);
WaitForExit(clientProcess, exitWait, 0);
processManager.WaitForExit(clientProcess, exitWait, 0);
});

return Task.CompletedTask;
Expand All @@ -92,19 +89,17 @@ protected async Task TestCrossProcessServerAsync(
int minListeners = 1,
Type? rmiServiceType = null)
{
InitializeProcessStartInfo(typeof(TestHost.Program), out ProcessStartInfo startInfo, out List<object> arguments);
ProcessManager processManager = new(typeof(TestHost.Program));
rmiServiceType ??= typeof(Tester);
arguments.Add(rmiServiceType.Assembly.Location);
arguments.Add(rmiServiceType.FullName!);
arguments.Add(serverPathPrefix);
arguments.Add(maxListeners);
arguments.Add(minListeners);
FinalizeProcessStartInfo(startInfo, arguments);

const int ExpectedExitCode = 12345;
using Process hostProcess = new();
hostProcess.StartInfo = startInfo;
hostProcess.Start().ShouldBeTrue();
processManager.Add(rmiServiceType.Assembly.Location);
processManager.Add(rmiServiceType.FullName!);
processManager.Add(serverPathPrefix);
processManager.Add(maxListeners);
processManager.Add(minListeners);

// Note: On Linux, exit codes must be in byte's range not int's! https://stackoverflow.com/a/51820986/1882616
const int ExpectedExitCode = 123;
using Process hostProcess = processManager.Start();
try
{
Thread.Sleep(2000);
Expand All @@ -123,7 +118,7 @@ protected async Task TestCrossProcessServerAsync(
finally
{
TimeSpan exitWait = TimeSpan.FromSeconds(10);
WaitForExit(hostProcess, exitWait, ExpectedExitCode);
processManager.WaitForExit(hostProcess, exitWait, ExpectedExitCode);
}
}

Expand Down Expand Up @@ -161,50 +156,4 @@ private protected static void WriteUnhandledServerException(Exception ex)
=> Console.WriteLine("ERROR: Unhandled server exception: " + ex);

#endregion

#region Private Methods

private static void InitializeProcessStartInfo(Type hostProgram, out ProcessStartInfo startInfo, out List<object> arguments)
{
string hostExeLocation = hostProgram.Assembly.Location;

startInfo = new()
{
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
};

arguments = [];
if (string.Equals(Path.GetExtension(hostExeLocation), ".exe", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = Path.GetFileName(hostExeLocation);
}
else
{
startInfo.FileName = Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\dotnet\dotnet.exe");
arguments.Add(hostExeLocation);
}
}

private static void FinalizeProcessStartInfo(ProcessStartInfo startInfo, List<object> arguments)
{
startInfo.Arguments = string.Join(" ", arguments.Select(arg => $"\"{arg}\""));
}

private static void WaitForExit(Process process, TimeSpan exitWait, int expectedExitCode, [CallerMemberName] string? caller = null)
{
if (process.WaitForExit((int)exitWait.TotalMilliseconds))
{
process.WaitForExit(); // Let console finish flushing.
process.ExitCode.ShouldBe(expectedExitCode);
}
else
{
process.Kill();
Assert.Fail($"{caller} process didn't exit within wait time of {exitWait}.");
}
}

#endregion
}
129 changes: 129 additions & 0 deletions tests/Menees.Remoting.Tests/ProcessManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
namespace Menees.Remoting;

#region Using Directives

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

#endregion

internal sealed class ProcessManager
{
#region Private Data Members

private readonly List<object> arguments = [];
private readonly List<string> output = [];
private readonly List<string> error = [];

#endregion

#region Constructors

public ProcessManager(Type hostProgram)
{
string hostExeLocation = hostProgram.Assembly.Location;

this.StartInfo = new()
{
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
};

if (string.Equals(Path.GetExtension(hostExeLocation), ".exe", StringComparison.OrdinalIgnoreCase))
{
this.StartInfo.FileName = Path.GetFileName(hostExeLocation);
}
else
{
this.StartInfo.FileName = OperatingSystem.IsWindows()

Check failure on line 44 in tests/Menees.Remoting.Tests/ProcessManager.cs

View workflow job for this annotation

GitHub Actions / build

'OperatingSystem' does not contain a definition for 'IsWindows'

Check failure on line 44 in tests/Menees.Remoting.Tests/ProcessManager.cs

View workflow job for this annotation

GitHub Actions / build

'OperatingSystem' does not contain a definition for 'IsWindows'

Check failure on line 44 in tests/Menees.Remoting.Tests/ProcessManager.cs

View workflow job for this annotation

GitHub Actions / build

'OperatingSystem' does not contain a definition for 'IsWindows'

Check failure on line 44 in tests/Menees.Remoting.Tests/ProcessManager.cs

View workflow job for this annotation

GitHub Actions / build

'OperatingSystem' does not contain a definition for 'IsWindows'
? Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\dotnet\dotnet.exe")
: "dotnet";
this.arguments.Add(hostExeLocation);
}
}

#endregion

#region Public Properties

public ProcessStartInfo StartInfo { get; }

#endregion

#region Public Methods

public void Add(object argument) => this.arguments.Add(argument);

public Process Start(bool redirectStreams = true)
{
this.StartInfo.Arguments = string.Join(" ", this.arguments.Select(arg => $"\"{arg}\""));
this.StartInfo.RedirectStandardOutput = redirectStreams;
this.StartInfo.RedirectStandardError = redirectStreams;

Process result = new() { StartInfo = this.StartInfo };
if (redirectStreams)
{
result.OutputDataReceived += (s, e) =>
{
lock (this.output)
{
this.output.Add(e.Data ?? string.Empty);
}
};

result.ErrorDataReceived += (s, e) =>
{
lock (this.error)
{
this.error.Add(e.Data ?? string.Empty);
}
};
}

result.Start().ShouldBeTrue();

if (redirectStreams)
{
result.BeginOutputReadLine();
result.BeginErrorReadLine();
}

return result;
}

public void WaitForExit(Process process, TimeSpan exitWait, int expectedExitCode, [CallerMemberName] string? caller = null)
{
if (process.WaitForExit((int)exitWait.TotalMilliseconds))
{
process.WaitForExit(); // Let console finish flushing.
this.WriteStreams();
process.ExitCode.ShouldBe(expectedExitCode);
}
else
{
this.WriteStreams();
process.Kill();
Assert.Fail($"{caller} process didn't exit within wait time of {exitWait}.");
}
}

#endregion

#region Private Methods

private void WriteStreams()
{
string output = string.Join(Environment.NewLine, this.output);
string error = string.Join(Environment.NewLine, this.error);
Debug.WriteLine(output);
Debug.WriteLine(error);
}

#endregion
}

0 comments on commit bc54156

Please sign in to comment.