-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Read asynchronously from test host process. #529
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client | |
using Microsoft.VisualStudio.TestPlatform.Utilities; | ||
|
||
using CrossPlatEngineResources = Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources.Resources; | ||
using System.Text; | ||
|
||
/// <summary> | ||
/// Base class for any operations that the client needs to drive through the engine. | ||
|
@@ -29,8 +30,12 @@ public abstract class ProxyOperationManager | |
|
||
private readonly int connectionTimeout; | ||
|
||
private readonly int errorLength; | ||
|
||
private readonly IProcessHelper processHelper; | ||
|
||
private StringBuilder testHostProcessStdError; | ||
|
||
#region Constructors | ||
|
||
/// <summary> | ||
|
@@ -39,12 +44,14 @@ public abstract class ProxyOperationManager | |
/// <param name="requestSender">Request Sender instance.</param> | ||
/// <param name="testHostManager">Test host manager instance.</param> | ||
/// <param name="clientConnectionTimeout">Client Connection Timeout.</param> | ||
protected ProxyOperationManager(ITestRequestSender requestSender, ITestHostManager testHostManager, int clientConnectionTimeout) | ||
protected ProxyOperationManager(ITestRequestSender requestSender, ITestHostManager testHostManager, int clientConnectionTimeout, int errorLength = 1000) | ||
{ | ||
this.RequestSender = requestSender; | ||
this.connectionTimeout = clientConnectionTimeout; | ||
this.testHostManager = testHostManager; | ||
this.processHelper = new ProcessHelper(); | ||
this.errorLength = errorLength; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instance variable is not used any where? |
||
testHostProcessStdError = new StringBuilder(errorLength, errorLength); | ||
this.initialized = false; | ||
} | ||
|
||
|
@@ -68,8 +75,6 @@ protected ProxyOperationManager(ITestRequestSender requestSender, ITestHostManag | |
/// <param name="sources">List of test sources.</param> | ||
public virtual void SetupChannel(IEnumerable<string> sources) | ||
{ | ||
string testHostProcessStdError = string.Empty; | ||
|
||
if (!this.initialized) | ||
{ | ||
var portNumber = this.RequestSender.InitializeCommunication(); | ||
|
@@ -82,16 +87,35 @@ public virtual void SetupChannel(IEnumerable<string> sources) | |
|
||
if (testHostStartInfo != null) | ||
{ | ||
// Monitor testhost exit. | ||
testHostStartInfo.ExitCallback = (process) => | ||
// Monitor testhost error callbacks. | ||
testHostStartInfo.ErrorReceivedCallback = (process, data) => | ||
{ | ||
if (process.ExitCode != 0) | ||
if(data != null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: move this to a method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
{ | ||
testHostProcessStdError = process.StandardError.ReadToEnd(); | ||
EqtTrace.Error("Test host exited with error: {0}", testHostProcessStdError); | ||
//if incoming data stream is huge empty entire testError stream, & limit data stream to MaxCapacity | ||
if (data.Length > testHostProcessStdError.MaxCapacity) | ||
{ | ||
testHostProcessStdError.Remove(0, testHostProcessStdError.Length); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which of these is most efficient?
|
||
data = data.Substring(data.Length - testHostProcessStdError.MaxCapacity); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're keeping the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are keeping last few only substring(index) which will give from starting index to end |
||
} | ||
|
||
//remove only what is required, from begining of error stream | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: spelling |
||
else | ||
{ | ||
int required = data.Length + testHostProcessStdError.Length - testHostProcessStdError.MaxCapacity; | ||
if (required > 0) | ||
{ | ||
testHostProcessStdError.Remove(0, required); | ||
} | ||
} | ||
|
||
testHostProcessStdError.Append(data); | ||
} | ||
|
||
this.RequestSender.OnClientProcessExit(testHostProcessStdError); | ||
if (process.HasExited && process.ExitCode != 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested process.HasExited out, the process object is valid, even after the actual process has ended. |
||
{ | ||
EqtTrace.Error("Test host exited with error: {0}", testHostProcessStdError); | ||
this.RequestSender.OnClientProcessExit(testHostProcessStdError.ToString()); | ||
} | ||
}; | ||
} | ||
|
||
|
@@ -112,7 +136,7 @@ public virtual void SetupChannel(IEnumerable<string> sources) | |
{ | ||
var errorMsg = CrossPlatEngineResources.InitializationFailed; | ||
|
||
if (!string.IsNullOrWhiteSpace(testHostProcessStdError)) | ||
if (!string.IsNullOrWhiteSpace(testHostProcessStdError.ToString())) | ||
{ | ||
// Testhost failed with error | ||
errorMsg = string.Format(CrossPlatEngineResources.TestHostExitedWithError, testHostProcessStdError); | ||
|
@@ -146,5 +170,14 @@ private string GetTimestampedLogFile(string logFile) | |
string.Format("host.{0}_{1}{2}", DateTime.Now.ToString("yy-MM-dd_HH-mm-ss_fffff"), | ||
Thread.CurrentThread.ManagedThreadId, Path.GetExtension(logFile))); | ||
} | ||
|
||
/// <summary> | ||
/// Returns the current error data in stream | ||
/// Written purely for UT as of now. | ||
/// </summary> | ||
public virtual string GetStandardError() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make it protected. Use a |
||
{ | ||
return testHostProcessStdError.ToString(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,10 +22,10 @@ internal class ProcessHelper : IProcessHelper | |
/// <param name="processPath">The path to the process.</param> | ||
/// <param name="arguments">Process arguments.</param> | ||
/// <param name="workingDirectory">Working directory of the process.</param> | ||
/// <param name="exitCallback"></param> | ||
/// <param name="errorCallback"></param> | ||
/// <returns>The process spawned.</returns> | ||
/// <exception cref="Exception">Throws any exception that could result as part of the launch.</exception> | ||
public Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action<Process> exitCallback) | ||
public Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action<Process, string> errorCallback) | ||
{ | ||
var process = new Process(); | ||
try | ||
|
@@ -39,13 +39,15 @@ public Process LaunchProcess(string processPath, string arguments, string workin | |
process.StartInfo.RedirectStandardError = true; | ||
process.EnableRaisingEvents = true; | ||
|
||
if (exitCallback != null) | ||
if (errorCallback != null) | ||
{ | ||
process.Exited += (sender, args) => exitCallback(sender as Process); | ||
process.ErrorDataReceived += (sender, args) => errorCallback(sender as Process, args.Data); | ||
} | ||
|
||
EqtTrace.Verbose("ProcessHelper: Starting process '{0}' with command line '{1}'", processPath, arguments); | ||
process.Start(); | ||
|
||
process.BeginErrorReadLine(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we require this even if |
||
} | ||
catch (Exception exception) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
namespace TestPlatform.CrossPlatEngine.UnitTests | ||
{ | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
using Moq; | ||
using System.Diagnostics; | ||
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Helpers.Interfaces; | ||
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client; | ||
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine; | ||
using System.Collections.Generic; | ||
using Microsoft.VisualStudio.TestPlatform.ObjectModel; | ||
using System; | ||
using System.Reflection; | ||
using System.Linq; | ||
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Helpers; | ||
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Hosting; | ||
|
||
[TestClass] | ||
public class ProcessHelperTests | ||
{ | ||
private readonly ProxyOperationManager testOperationManager; | ||
|
||
private readonly TestableTestHostManager testHostManager; | ||
|
||
private readonly Mock<ITestRequestSender> mockRequestSender; | ||
|
||
private TestableProcessHelper processHelper; | ||
|
||
private int connectionTimeout = 400; | ||
|
||
private int errorLength = 20; | ||
|
||
public ProcessHelperTests() | ||
{ | ||
this.mockRequestSender = new Mock<ITestRequestSender>(); | ||
this.mockRequestSender.Setup(rs => rs.WaitForRequestHandlerConnection(this.connectionTimeout)).Returns(true); | ||
|
||
this.processHelper = new TestableProcessHelper(); | ||
this.testHostManager = new TestableTestHostManager(Architecture.X64, Framework.DefaultFramework, this.processHelper, true); | ||
|
||
this.testOperationManager = new TestableProxyOperationManager(this.mockRequestSender.Object, this.testHostManager, this.connectionTimeout, this.errorLength); | ||
} | ||
|
||
[TestMethod] | ||
public void ErrorMessageShouldBeReadAsynchronously() | ||
{ | ||
string errorData = "Custom Error Strings"; | ||
this.processHelper.SetErrorMessage(errorData); | ||
|
||
this.mockRequestSender.Setup(rs => rs.InitializeCommunication()).Returns(123); | ||
|
||
this.testOperationManager.SetupChannel(Enumerable.Empty<string>()); | ||
|
||
Assert.AreEqual(this.testOperationManager.GetStandardError(), errorData); | ||
} | ||
|
||
[TestMethod] | ||
public void ErrorMessageShouldBeTruncatedToMatchErrorLength() | ||
{ | ||
string errorData = "Long Custom Error Strings"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest to organize unit tests as follows for readability (comments are not required, newlines are only between blocks):
|
||
this.processHelper.SetErrorMessage(errorData); | ||
|
||
this.mockRequestSender.Setup(rs => rs.InitializeCommunication()).Returns(123); | ||
|
||
this.testOperationManager.SetupChannel(Enumerable.Empty<string>()); | ||
|
||
Assert.AreEqual(this.testOperationManager.GetStandardError().Length, errorLength); | ||
|
||
Assert.AreEqual(this.testOperationManager.GetStandardError(), errorData.Substring(5)); | ||
} | ||
|
||
[TestMethod] | ||
public void ErrorMessageShouldBeTruncatedFromBeginingShouldDisplayTrailingData() | ||
{ | ||
string errorData = "Error Strings"; | ||
this.processHelper.SetErrorMessage(errorData); | ||
|
||
this.mockRequestSender.Setup(rs => rs.InitializeCommunication()).Returns(123); | ||
|
||
this.testOperationManager.SetupChannel(Enumerable.Empty<string>()); | ||
|
||
Assert.AreEqual(this.testOperationManager.GetStandardError(), "StringsError Strings"); | ||
} | ||
|
||
private class TestableProxyOperationManager : ProxyOperationManager | ||
{ | ||
public TestableProxyOperationManager( | ||
ITestRequestSender requestSender, | ||
ITestHostManager testHostManager, | ||
int clientConnectionTimeout, | ||
int errorLength) : base(requestSender, testHostManager, clientConnectionTimeout, errorLength) | ||
{ | ||
} | ||
|
||
public override string GetStandardError() | ||
{ | ||
return base.GetStandardError(); | ||
} | ||
} | ||
|
||
private class TestableTestHostManager : DefaultTestHostManager | ||
{ | ||
public TestableTestHostManager( | ||
Architecture architecture, | ||
Framework framework, | ||
IProcessHelper processHelper, | ||
bool shared) : base(architecture, framework, processHelper, shared) | ||
{ | ||
} | ||
|
||
public override TestProcessStartInfo GetTestHostProcessStartInfo(IEnumerable<string> sources, IDictionary<string, string> environmentVariables, TestRunnerConnectionInfo connectionInfo) | ||
{ | ||
return new TestProcessStartInfo(); | ||
} | ||
} | ||
private class TestableProcessHelper : IProcessHelper | ||
{ | ||
private string ErrorMessage; | ||
|
||
public void SetErrorMessage(string errorMessage) | ||
{ | ||
this.ErrorMessage = errorMessage; | ||
} | ||
public string GetCurrentProcessFileName() | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public int GetCurrentProcessId() | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public string GetTestEngineDirectory() | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action<Process, string> errorCallback) | ||
{ | ||
var process = Process.GetCurrentProcess(); | ||
|
||
errorCallback(process, this.ErrorMessage); | ||
errorCallback(process, this.ErrorMessage); | ||
errorCallback(process, this.ErrorMessage); | ||
errorCallback(process, this.ErrorMessage); | ||
|
||
return process; | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explicit arguments are better. Please call with
1000
from the public ctor.