Skip to content
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

Execute Boost.Test processes in an async manner #100

Open
wants to merge 1 commit into
base: dev15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions BoostTestAdapter/Boost/Runner/BoostTest162Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

using BoostTestAdapter.Utility;
using BoostTestAdapter.Utility.ExecutionContext;
using System;
using System.IO;
using System.Globalization;
using BoostTestAdapter.Utility.ExecutionContext;
using BoostTestAdapter.Utility;

using System.IO;
using System.Threading;
using System.Threading.Tasks;
using static BoostTestAdapter.BoostTestExecutor;

namespace BoostTestAdapter.Boost.Runner
Expand Down Expand Up @@ -43,7 +44,7 @@ public BoostTest162Runner(IBoostTestRunner runner)

public string Source => this.Runner.Source;

public int Execute(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext)
public async Task<int> ExecuteAsync(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext, CancellationToken token)
{
var fixedArgs = args;

Expand All @@ -55,7 +56,12 @@ public int Execute(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings

using (var stderr = new TemporaryFile(IsStandardErrorFileDifferent(args, fixedArgs) ? fixedArgs.StandardErrorFile : null))
{
int resultCode = this.Runner.Execute(fixedArgs, settings, executionContext);
var resultCode = await Runner.ExecuteAsync(fixedArgs, settings, executionContext, token);

if (token.IsCancellationRequested)
{
return resultCode;
}

// Extract the report output to its intended location
string source = (fixedArgs == null) ? null : fixedArgs.StandardErrorFile;
Expand Down Expand Up @@ -115,7 +121,7 @@ private BoostTestRunnerCommandLineArgs AdaptArguments(BoostTestRunnerCommandLine
args.Report = Sink.StandardError;
if (string.IsNullOrEmpty(args.StandardErrorFile))
{
args.StandardErrorFile = TestPathGenerator.Generate(this.Source, FileExtensions.StdErrFile);
args.StandardErrorFile = TestPathGenerator.Generate(Source, FileExtensions.StdErrFile);
}

return args;
Expand Down
82 changes: 42 additions & 40 deletions BoostTestAdapter/Boost/Runner/BoostTestRunnerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

// This file has been modified by Microsoft on 8/2017.

using BoostTestAdapter.Utility;
using BoostTestAdapter.Utility.ExecutionContext;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Management;
using BoostTestAdapter.Utility;
using System.ComponentModel;
using BoostTestAdapter.Utility.ExecutionContext;
using System.Threading;
using System.Threading.Tasks;

namespace BoostTestAdapter.Boost.Runner
{
Expand Down Expand Up @@ -46,15 +48,48 @@ protected BoostTestRunnerBase(string testRunnerExecutable)

#region IBoostTestRunner

public virtual int Execute(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext)
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
public Task<int> ExecuteAsync(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext, CancellationToken token)
{
Utility.Code.Require(settings, "settings");
Utility.Code.Require(executionContext, "executionContext");

using (Process process = executionContext.LaunchProcess(GetExecutionContextArgs(args, settings)))
var source = new TaskCompletionSource<int>();
var process = executionContext.LaunchProcess(GetExecutionContextArgs(args, settings));

process.Exited += (object obj, EventArgs ev) =>
{
return MonitorProcess(process, settings.Timeout);
try
{
source.TrySetResult(process.ExitCode);
}
catch (Exception ex)
{
source.TrySetException(ex);
}
};

try
{
process.EnableRaisingEvents = true;
}
catch (Exception ex)
{
source.TrySetException(ex);
}

token.Register(() => { source.TrySetCanceled(); });

return source.Task.ContinueWith((Task<int> result) =>
{
if (result.Status != TaskStatus.RanToCompletion)
{
KillProcessIncludingChildren(process);
}

process.Dispose();

return result.Result;
});
}

public virtual string Source
Expand Down Expand Up @@ -119,39 +154,6 @@ protected virtual ProcessExecutionContextArgs GetExecutionContextArgs(BoostTestR

#endregion IBoostTestRunner

/// <summary>
/// Monitors the provided process for the specified timeout.
/// </summary>
/// <param name="process">The process to monitor.</param>
/// <param name="timeout">The timeout threshold until the process and its children should be killed.</param>
/// <exception cref="TimeoutException">Thrown in case specified timeout threshold is exceeded.</exception>
private static int MonitorProcess(Process process, int timeout)
{
process.WaitForExit(timeout);

if (!process.HasExited)
{
KillProcessIncludingChildren(process);

throw new TimeoutException(timeout);
}

try
{
return process.ExitCode;
}
catch (InvalidOperationException)
{
// This is a common scenario when attempting to request the exit code
// of a process which is executed through the debugger. In such cases
// assume a successful exit scenario. Should this not be the case, the
// adapter will 'naturally' fail in other instances e.g. when attempting
// to read test reports.

return 0;
}
}

/// <summary>
/// Kills a process identified by its pid and all its children processes
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions BoostTestAdapter/Boost/Runner/IBoostTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// http://www.boost.org/LICENSE_1_0.txt)

using BoostTestAdapter.Utility.ExecutionContext;
using System.Threading;
using System.Threading.Tasks;

namespace BoostTestAdapter.Boost.Runner
{
Expand All @@ -18,10 +20,10 @@ public interface IBoostTestRunner
/// <param name="args">The Boost Test framework command line options.</param>
/// <param name="settings">The Boost Test runner settings.</param>
/// <param name="executionContext">An IProcessExecutionContext which will manage any spawned process.</param>
/// <param name="token">A cancellation token to suspend test execution.</param>
/// <returns>Boost.Test result code</returns>
/// <exception cref="TimeoutException">Thrown in case specified timeout threshold is exceeded.</exception>
int Execute(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext);

Task<int> ExecuteAsync(BoostTestRunnerCommandLineArgs args, BoostTestRunnerSettings settings, IProcessExecutionContext executionContext, CancellationToken token);

/// <summary>
/// Provides a source Id distinguishing different instances
/// </summary>
Expand Down
86 changes: 71 additions & 15 deletions BoostTestAdapter/BoostTestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ namespace BoostTestAdapter
/// Implementation of ITestExecutor interface for Boost Tests.
/// </summary>
[ExtensionUri(ExecutorUriString)]
public class BoostTestExecutor : ITestExecutor
public class BoostTestExecutor : ITestExecutor, IDisposable
{
#region Constants

Expand Down Expand Up @@ -68,8 +68,6 @@ public BoostTestExecutor()
_testRunnerFactory = new DefaultBoostTestRunnerFactory();
_boostTestDiscovererFactory = new BoostTestDiscovererFactory(_testRunnerFactory);
_packageServiceFactory = new DefaultBoostTestPackageServiceFactory();

_cancelled = false;
}

/// <summary>
Expand All @@ -83,8 +81,6 @@ public BoostTestExecutor(IBoostTestRunnerFactory testRunnerFactory, IBoostTestDi
_testRunnerFactory = testRunnerFactory;
_boostTestDiscovererFactory = boostTestDiscovererFactory;
_packageServiceFactory = packageServiceFactory;

_cancelled = false;
}

#endregion Constructors
Expand All @@ -94,7 +90,7 @@ public BoostTestExecutor(IBoostTestRunnerFactory testRunnerFactory, IBoostTestDi
/// <summary>
/// Cancel flag
/// </summary>
private volatile bool _cancelled;
private CancellationTokenSource _cancelled = null;

/// <summary>
/// Boost Test Discoverer Factory - provisions test discoverers
Expand Down Expand Up @@ -123,15 +119,16 @@ private void SetUp(IMessageLogger logger)
System.Diagnostics.Debugger.Launch();
#endif

_cancelled = false;
_cancelled = new CancellationTokenSource();
Logger.Initialize(logger);
}

/// <summary>
/// Termination/Cleanup routine for running tests
/// </summary>
private static void TearDown()
private void TearDown()
{
_cancelled.Dispose();
Logger.Shutdown();
}

Expand Down Expand Up @@ -190,7 +187,7 @@ public void RunTests(IEnumerable<string> sources,

foreach (string source in sources)
{
if (_cancelled)
if (_cancelled.IsCancellationRequested)
{
break;
}
Expand Down Expand Up @@ -294,7 +291,7 @@ public void RunTests(IEnumerable<VSTestCase> tests, IRunContext runContext, IFra
/// </summary>
public void Cancel()
{
_cancelled = true;
_cancelled.Cancel();
}

#endregion ITestExecutor
Expand Down Expand Up @@ -341,7 +338,7 @@ private void RunBoostTests(IEnumerable<TestRun> testBatches, IRunContext runCont

foreach (TestRun batch in testBatches)
{
if (_cancelled)
if (_cancelled.IsCancellationRequested)
{
break;
}
Expand All @@ -350,6 +347,11 @@ private void RunBoostTests(IEnumerable<TestRun> testBatches, IRunContext runCont

try
{
foreach (var test in batch.Tests)
{
frameworkHandle.RecordStart(test);
}

Logger.Info(((runContext.IsBeingDebugged) ? Resources.Debugging : Resources.Executing), string.Join(", ", batch.Tests));

using (TemporaryFile report = new TemporaryFile(batch.Arguments.ReportFile))
Expand All @@ -372,9 +374,11 @@ private void RunBoostTests(IEnumerable<TestRun> testBatches, IRunContext runCont
{
Thread.Sleep(settings.PostTestDelay);
}

foreach (VSTestResult result in GenerateTestResults(batch, start, settings))
{
frameworkHandle.RecordEnd(result.TestCase, result.Outcome);

// Identify test result to Visual Studio Test framework
frameworkHandle.RecordResult(result);
}
Expand All @@ -388,6 +392,7 @@ private void RunBoostTests(IEnumerable<TestRun> testBatches, IRunContext runCont
VSTestResult testResult = GenerateTimeoutResult(testCase, ex);
testResult.StartTime = start;

frameworkHandle.RecordEnd(testResult.TestCase, testResult.Outcome);
frameworkHandle.RecordResult(testResult);
}
}
Expand All @@ -405,21 +410,46 @@ private void RunBoostTests(IEnumerable<TestRun> testBatches, IRunContext runCont
/// <param name="runContext">The RunContext for this TestCase. Determines whether the test should be debugged or not.</param>
/// <param name="frameworkHandle">The FrameworkHandle for this test execution instance.</param>
/// <returns></returns>
private static bool ExecuteTests(TestRun run, IRunContext runContext, IFrameworkHandle frameworkHandle)
private bool ExecuteTests(TestRun run, IRunContext runContext, IFrameworkHandle frameworkHandle)
{
if (run.Runner != null)
{
using (var context = CreateExecutionContext(runContext, frameworkHandle))
using (var cancel = new CancellationTokenSource())
{
run.Execute(context);
// Associate the test-batch local cancellation source to the global cancellation source
// so that if the global source is canceled, the local source is also canceled
_cancelled.Token.Register(cancel.Cancel);

try
{
if (!run.ExecuteAsync(context, cancel.Token).Wait(run.Settings.Timeout))
{
cancel.Cancel();
throw new Boost.Runner.TimeoutException(run.Settings.Timeout);
}
}
catch (AggregateException)
{
// Suppress internal task exceptions or cancellations.
//
// This is a common scenario when attempting to request the exit code
// of a process which is executed through the debugger. In such cases
// assume a successful exit scenario. Should this not be the case, the
// adapter will 'naturally' fail in other instances e.g. when attempting
// to read test reports.
}

// This will return false in case the global cancellation source has been canceled
return !cancel.IsCancellationRequested;
}
}
else
{
Logger.Error(Resources.ExecutorNotFound, string.Join(", ", run.Tests));
}

return run.Runner != null;
return false;
}

/// <summary>
Expand Down Expand Up @@ -664,5 +694,31 @@ private static IProcessExecutionContext CreateExecutionContext(IRunContext conte
}

#endregion Helper methods

#region IDisposable Support

private bool disposedValue = false; // To detect redundant calls

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_cancelled.Dispose();
}

disposedValue = true;
}
}

// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
}

#endregion
}
}
Loading