Skip to content

Commit

Permalink
Execute Boost.Test processes in an async manner
Browse files Browse the repository at this point in the history
- Record test execution start and end
  • Loading branch information
Brian Gatt committed Oct 22, 2018
1 parent f0675bb commit f75cbec
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 99 deletions.
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

0 comments on commit f75cbec

Please sign in to comment.