diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs index bd0aab478e..d8652f260b 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs @@ -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; /// /// Base class for any operations that the client needs to drive through the engine. @@ -31,6 +32,10 @@ public abstract class ProxyOperationManager private readonly IProcessHelper processHelper; + private StringBuilder testHostProcessStdError; + + protected int ErrorLength { get; set; } = 1000; + #region Constructors /// @@ -68,10 +73,9 @@ protected ProxyOperationManager(ITestRequestSender requestSender, ITestHostManag /// List of test sources. public virtual void SetupChannel(IEnumerable sources) { - string testHostProcessStdError = string.Empty; - if (!this.initialized) { + this.testHostProcessStdError = new StringBuilder(ErrorLength, ErrorLength); var portNumber = this.RequestSender.InitializeCommunication(); var processId = this.processHelper.GetCurrentProcessId(); var connectionInfo = new TestRunnerConnectionInfo { Port = portNumber, RunnerProcessId = processId, LogFile = this.GetTimestampedLogFile(EqtTrace.LogFile) }; @@ -82,16 +86,35 @@ public virtual void SetupChannel(IEnumerable 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) { - 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.Clear(); + data = data.Substring(data.Length - testHostProcessStdError.MaxCapacity); + } + + //remove only what is required, from beginning of error stream + 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) + { + EqtTrace.Error("Test host exited with error: {0}", testHostProcessStdError); + this.RequestSender.OnClientProcessExit(testHostProcessStdError.ToString()); + } }; } @@ -112,7 +135,7 @@ public virtual void SetupChannel(IEnumerable 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 +169,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))); } + + /// + /// Returns the current error data in stream + /// Written purely for UT as of now. + /// + protected virtual string GetStandardError() + { + return testHostProcessStdError.ToString(); + } } } diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/Interfaces/IProcessHelper.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/Interfaces/IProcessHelper.cs index 772b3089f5..4f11a32752 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/Interfaces/IProcessHelper.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/Interfaces/IProcessHelper.cs @@ -19,7 +19,7 @@ internal interface IProcessHelper /// The working directory for this process. /// Call back for on process exit /// The process created. - Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action exitCallback); + Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action errorCallback); /// /// Gets the current process file path. diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/ProcessHelper.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/ProcessHelper.cs index a74f730a74..f7d865bf48 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/ProcessHelper.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Helpers/ProcessHelper.cs @@ -22,10 +22,10 @@ internal class ProcessHelper : IProcessHelper /// The path to the process. /// Process arguments. /// Working directory of the process. - /// + /// /// The process spawned. /// Throws any exception that could result as part of the launch. - public Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action exitCallback) + public Process LaunchProcess(string processPath, string arguments, string workingDirectory, Action 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(); } catch (Exception exception) { diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DefaultTestHostManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DefaultTestHostManager.cs index e5151220ad..8cf445a9fd 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DefaultTestHostManager.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DefaultTestHostManager.cs @@ -98,7 +98,7 @@ public int LaunchTestHost(TestProcessStartInfo testHostStartInfo) if (this.customTestHostLauncher == null) { - this.testHostProcess = this.processHelper.LaunchProcess(testHostStartInfo.FileName, testHostStartInfo.Arguments, testHostStartInfo.WorkingDirectory, testHostStartInfo.ExitCallback); + this.testHostProcess = this.processHelper.LaunchProcess(testHostStartInfo.FileName, testHostStartInfo.Arguments, testHostStartInfo.WorkingDirectory, testHostStartInfo.ErrorReceivedCallback); } else { diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DotnetTestHostManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DotnetTestHostManager.cs index c8c9339d0b..8d54658ed0 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DotnetTestHostManager.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Hosting/DotnetTestHostManager.cs @@ -313,7 +313,7 @@ public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo) defaultTestHostStartInfo.FileName, defaultTestHostStartInfo.Arguments, defaultTestHostStartInfo.WorkingDirectory, - defaultTestHostStartInfo.ExitCallback).Id; + defaultTestHostStartInfo.ErrorReceivedCallback).Id; } } } diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestProcessStartInfo.cs b/src/Microsoft.TestPlatform.ObjectModel/TestProcessStartInfo.cs index 65ab8c7d55..69fff3d581 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/TestProcessStartInfo.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/TestProcessStartInfo.cs @@ -41,6 +41,6 @@ public class TestProcessStartInfo /// /// Callback on process exit /// - public Action ExitCallback { get; set; } + public Action ErrorReceivedCallback { get; set; } } } diff --git a/src/VSIXProject/source.extension.vsixmanifest b/src/VSIXProject/source.extension.vsixmanifest index 1e148bd5b7..0a6bfdcda6 100644 --- a/src/VSIXProject/source.extension.vsixmanifest +++ b/src/VSIXProject/source.extension.vsixmanifest @@ -1,7 +1,7 @@ - + Microsoft Visual Studio Test Platform Extensible Test Platform that allows developers to discover, execute and analyze automated tests. License.rtf @@ -25,3 +25,4 @@ + diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/ProxyOperationManagerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/ProxyOperationManagerTests.cs index c1c9df1ad1..3b8aca0f1c 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/ProxyOperationManagerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Client/ProxyOperationManagerTests.cs @@ -153,7 +153,7 @@ public void SetupChannelShouldAddExitCallbackToTestHostStartInfo() this.testOperationManager.SetupChannel(Enumerable.Empty()); - Assert.IsNotNull(startInfo.ExitCallback); + Assert.IsNotNull(startInfo.ErrorReceivedCallback); } [TestMethod] diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Helpers/ProcessHelperTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Helpers/ProcessHelperTests.cs new file mode 100644 index 0000000000..037454f617 --- /dev/null +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Helpers/ProcessHelperTests.cs @@ -0,0 +1,153 @@ +// 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 TestableProxyOperationManager testOperationManager; + + private readonly TestableTestHostManager testHostManager; + + private readonly Mock mockRequestSender; + + private TestableProcessHelper processHelper; + + private int connectionTimeout = 400; + + private int errorLength = 20; + + public ProcessHelperTests() + { + this.mockRequestSender = new Mock(); + 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()); + + Assert.AreEqual(this.testOperationManager.GetError(), errorData); + } + + [TestMethod] + public void ErrorMessageShouldBeTruncatedToMatchErrorLength() + { + string errorData = "Long Custom Error Strings"; + this.processHelper.SetErrorMessage(errorData); + this.mockRequestSender.Setup(rs => rs.InitializeCommunication()).Returns(123); + + this.testOperationManager.SetupChannel(Enumerable.Empty()); + + Assert.AreEqual(this.testOperationManager.GetError().Length, errorLength); + Assert.AreEqual(this.testOperationManager.GetError(), 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()); + + Assert.AreEqual(this.testOperationManager.GetError(), "StringsError Strings"); + } + + private class TestableProxyOperationManager : ProxyOperationManager + { + public TestableProxyOperationManager( + ITestRequestSender requestSender, + ITestHostManager testHostManager, + int clientConnectionTimeout, + int errorLength) : base(requestSender, testHostManager, clientConnectionTimeout) + { + base.ErrorLength = errorLength; + } + + public string GetError() + { + 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 sources, IDictionary 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 errorCallback) + { + var process = Process.GetCurrentProcess(); + + errorCallback(process, this.ErrorMessage); + errorCallback(process, this.ErrorMessage); + errorCallback(process, this.ErrorMessage); + errorCallback(process, this.ErrorMessage); + + return process; + } + } + } +}