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

Support for CancellationTokenSource in TestContext to help in timeout… #585

Merged
merged 4 commits into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
48 changes: 26 additions & 22 deletions src/Adapter/MSTest.CoreAdapter/Execution/TestMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Threading;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
Expand Down Expand Up @@ -453,8 +454,8 @@ private void RunTestCleanupMethod(object classInstance, TestResult result)

if (cleanupStackTrace.Length > 0)
{
cleanupStackTrace.Append(Resource.UTA_CleanupStackTrace);
cleanupStackTrace.Append(Environment.NewLine);
cleanupStackTrace.Append(Resource.UTA_CleanupStackTrace);
cleanupStackTrace.Append(Environment.NewLine);
}

Exception realException = ex.GetInnerExceptionOrDefault();
Expand Down Expand Up @@ -668,19 +669,20 @@ private TestResult ExecuteInternalWithTimeout(object[] arguments)
TestResult result = null;
Exception failure = null;

Action executeAsyncAction = () =>
void executeAsyncAction()
{
try
{
try
{
result = this.ExecuteInternal(arguments);
}
catch (Exception ex)
{
failure = ex;
}
};
result = this.ExecuteInternal(arguments);
}
catch (Exception ex)
{
failure = ex;
}
}

if (PlatformServiceProvider.Instance.ThreadOperations.Execute(executeAsyncAction, this.TestMethodOptions.Timeout))
CancellationToken cancelToken = this.TestMethodOptions.TestContext.Context.CancellationTokenSource.Token;
if (PlatformServiceProvider.Instance.ThreadOperations.Execute(executeAsyncAction, this.TestMethodOptions.Timeout, cancelToken))
{
if (failure != null)
{
Expand All @@ -692,16 +694,18 @@ private TestResult ExecuteInternalWithTimeout(object[] arguments)
}
else
{
// Timed out

// If the method times out, then
//
// 1. If the test is stuck, then we can get CannotUnloadAppDomain exception.
//
// Which are handled as follows: -
//
// For #1, we are now restarting the execution process if adapter fails to unload app-domain.
// Timed out or canceled
string errorMessage = string.Format(CultureInfo.CurrentCulture, Resource.Execution_Test_Timeout, this.TestMethodName);
cltshivash marked this conversation as resolved.
Show resolved Hide resolved
if (this.TestMethodOptions.TestContext.Context.CancellationTokenSource.IsCancellationRequested)
{
errorMessage = string.Format(CultureInfo.CurrentCulture, Resource.Execution_Test_Cancelled, this.TestMethodName);
}
else
{
// Cancel the token source
cltshivash marked this conversation as resolved.
Show resolved Hide resolved
this.TestMethodOptions.TestContext.Context.CancellationTokenSource.Cancel();
}

TestResult timeoutResult = new TestResult() { Outcome = TestTools.UnitTesting.UnitTestOutcome.Timeout, TestFailureException = new TestFailedException(UnitTestOutcome.Timeout, errorMessage) };
return timeoutResult;
}
Expand Down
15 changes: 8 additions & 7 deletions src/Adapter/MSTest.CoreAdapter/Execution/TestMethodRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,14 @@ internal UnitTestResult[] Execute()
result = new[] { new UnitTestResult() };
}

var newResult =
new UnitTestResult(new TestFailedException(UnitTestOutcome.Error, ex.TryGetMessage(), ex.TryGetStackTraceInformation()));
newResult.StandardOut = result[result.Length - 1].StandardOut;
newResult.StandardError = result[result.Length - 1].StandardError;
newResult.DebugTrace = result[result.Length - 1].DebugTrace;
newResult.TestContextMessages = result[result.Length - 1].TestContextMessages;
newResult.Duration = result[result.Length - 1].Duration;
var newResult = new UnitTestResult(new TestFailedException(UnitTestOutcome.Error, ex.TryGetMessage(), ex.TryGetStackTraceInformation()))
{
StandardOut = result[result.Length - 1].StandardOut,
StandardError = result[result.Length - 1].StandardError,
DebugTrace = result[result.Length - 1].DebugTrace,
TestContextMessages = result[result.Length - 1].TestContextMessages,
Duration = result[result.Length - 1].Duration
};
result[result.Length - 1] = newResult;
}
finally
Expand Down
9 changes: 9 additions & 0 deletions src/Adapter/MSTest.CoreAdapter/Resources/Resource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Adapter/MSTest.CoreAdapter/Resources/Resource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,7 @@ Error: {1}</value>
<data name="UTA_TestMethodExpectedParameters" xml:space="preserve">
<value>Only data driven test methods can have parameters. Did you intend to use [DataRow] or [DynamicData]?</value>
</data>
<data name="Execution_Test_Cancelled" xml:space="preserve">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please xlf file changes associated with this change

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run command "build.cmd -uxlf". You would have to commit the xlf files changes post running the script.

<value>Test '{0}' execution has been aborted.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices
using System.Threading;

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;

#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName

Expand All @@ -21,36 +22,44 @@ public class ThreadOperations : IThreadOperations
/// </summary>
/// <param name="action">The action to execute.</param>
/// <param name="timeout">Timeout for the specified action in milliseconds.</param>
/// <param name="cancelToken">Token to cancel the execution</param>
/// <returns>Returns true if the action executed before the timeout. returns false otherwise.</returns>
public bool Execute(Action action, int timeout)
public bool Execute(Action action, int timeout, CancellationToken cancelToken)
{
Thread executionThread = new Thread(new ThreadStart(action));
executionThread.IsBackground = true;
executionThread.Name = "MSTestAdapter Thread";
bool executionAborted = false;
Thread executionThread = new Thread(new ThreadStart(action))
{
IsBackground = true,
Name = "MSTestAdapter Thread"
};

executionThread.SetApartmentState(Thread.CurrentThread.GetApartmentState());
executionThread.Start();
cancelToken.Register(() =>
{
executionAborted = true;
AbortThread(executionThread);
});

if (executionThread.Join(timeout))
if (JoinThread(timeout, executionThread))
{
if (executionAborted)
{
return false;
}

// Successfully completed
return true;
}
else if (executionAborted)
{
// Execution aborted due to user choice
return false;
}
else
{
// Timed out
try
{
// Abort test thread after timeout.
executionThread.Abort();
}
catch (ThreadStateException)
{
// Catch and discard ThreadStateException. If Abort is called on a thread that has been suspended,
// a ThreadStateException is thrown in the thread that called Abort,
// and AbortRequested is added to the ThreadState property of the thread being aborted.
// A ThreadAbortException is not thrown in the suspended thread until Resume is called.
}

AbortThread(executionThread);
return false;
}
}
Expand All @@ -75,7 +84,39 @@ public void ExecuteWithAbortSafety(Action action)
throw new TargetInvocationException(exception);
}
}
}

private static bool JoinThread(int timeout, Thread executionThread)
{
try
{
return executionThread.Join(timeout);
}
catch (ThreadStateException)
{
// Catch and discard ThreadStateException. If Abort is called on a thread that has been suspended,
cltshivash marked this conversation as resolved.
Show resolved Hide resolved
// a ThreadStateException is thrown in the thread that called Abort,
// and AbortRequested is added to the ThreadState property of the thread being aborted.
// A ThreadAbortException is not thrown in the suspended thread until Resume is called.
}

return false;
}

private static void AbortThread(Thread executionThread)
{
try
{
// Abort test thread after timeout.
executionThread.Abort();
}
catch (ThreadStateException)
{
// Catch and discard ThreadStateException. If Abort is called on a thread that has been suspended,
// a ThreadStateException is thrown in the thread that called Abort,
// and AbortRequested is added to the ThreadState property of the thread being aborted.
// A ThreadAbortException is not thrown in the suspended thread until Resume is called.
}
}
}
#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName
}
4 changes: 3 additions & 1 deletion src/Adapter/PlatformServices.Interface/IThreadOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface
{
using System;
using System.Threading;

/// <summary>
/// This service is responsible for any thread operations specific to a platform.
Expand All @@ -15,8 +16,9 @@ public interface IThreadOperations
/// </summary>
/// <param name="action">The action to execute.</param>
/// <param name="timeout">Timeout for the specified action.</param>
/// <param name="cancelToken">Token to cancel the execution</param>
/// <returns>Returns true if the action executed before the timeout. returns false otherwise.</returns>
bool Execute(Action action, int timeout);
bool Execute(Action action, int timeout, CancellationToken cancelToken);

/// <summary>
/// Execute an action with handling for Thread Aborts (if possible) so the main thread of the adapter does not die.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices
using System.Diagnostics;
using System.Globalization;
using System.IO;

using System.Threading;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel;

Expand Down Expand Up @@ -61,6 +61,7 @@ public TestContextImplementation(ITestMethod testMethod, StringWriter writer, ID
this.testMethod = testMethod;
this.properties = new Dictionary<string, object>(properties);
this.stringWriter = writer;
this.CancellationTokenSource = new CancellationTokenSource();
cltshivash marked this conversation as resolved.
Show resolved Hide resolved
this.InitializeProperties();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices
{
using System;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
Expand All @@ -20,12 +21,12 @@ public class ThreadOperations : IThreadOperations
/// </summary>
/// <param name="action">The action to execute.</param>
/// <param name="timeout">Timeout for the specified action.</param>
/// <param name="cancelToken">Token to cancel the execution</param>
/// <returns>Returns true if the action executed before the timeout. returns false otherwise.</returns>
public bool Execute(Action action, int timeout)
public bool Execute(Action action, int timeout, CancellationToken cancelToken)
{
var executionTask = Task.Factory.StartNew(action);

if (executionTask.Wait(timeout))
if (executionTask.Wait(timeout, cancelToken))
{
return true;
}
Expand Down
6 changes: 6 additions & 0 deletions src/TestFramework/Extension.Shared/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;

/// <summary>
/// TestContext class. This class should be fully abstract and not contain any
Expand All @@ -21,6 +22,11 @@ public abstract class TestContext
/// </summary>
public abstract IDictionary Properties { get; }

/// <summary>
/// Gets or sets the cancellation token source that is cancelled when test timesout or when cancelled the test will be aborted
cltshivash marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public CancellationTokenSource CancellationTokenSource { get; protected set; }
cltshivash marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets Fully-qualified name of the class containing the test method currently being executed
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
Expand Down Expand Up @@ -1221,7 +1222,7 @@ public void TestMethodInfoInvokeShouldReturnTestFailureOnTimeout()
PlatformServiceProvider.Instance = testablePlatformServiceProvider;

testablePlatformServiceProvider.MockThreadOperations.Setup(
to => to.Execute(It.IsAny<Action>(), It.IsAny<int>())).Returns(false);
to => to.Execute(It.IsAny<Action>(), It.IsAny<int>(), It.IsAny<CancellationToken>())).Returns(false);
this.testMethodOptions.Timeout = 1;
var method = new TestMethodInfo(this.methodInfo, this.testClassInfo, this.testMethodOptions);

Expand Down Expand Up @@ -1256,7 +1257,7 @@ private void RunWithTestablePlatformService(TestablePlatformServiceProvider test
try
{
testablePlatformServiceProvider.MockThreadOperations.
Setup(tho => tho.Execute(It.IsAny<Action>(), It.IsAny<int>())).
Setup(tho => tho.Execute(It.IsAny<Action>(), It.IsAny<int>(), It.IsAny<CancellationToken>())).
Returns(true).
Callback((Action a, int timeout) =>
{
Expand Down
Loading