-
Notifications
You must be signed in to change notification settings - Fork 723
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for TimeoutAttribute for .NET 8.0
Also support Abort/Kill on NET8 .NET 6.0 now throws PlatformNotSupportedException
- Loading branch information
1 parent
e10e269
commit a78f254
Showing
25 changed files
with
472 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 10 additions & 108 deletions
118
src/NUnitFramework/framework/Internal/Commands/TimeoutCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,32 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
|
||
#if THREAD_ABORT | ||
using System.Threading; | ||
#else | ||
using System; | ||
using System.Threading.Tasks; | ||
#endif | ||
using NUnit.Framework.Interfaces; | ||
using System.Runtime; | ||
using NUnit.Framework.Internal.Abstractions; | ||
|
||
namespace NUnit.Framework.Internal.Commands | ||
{ | ||
/// <summary> | ||
/// <see cref="TimeoutCommand"/> creates a timer in order to cancel | ||
/// a test if it exceeds a specified time and adjusts | ||
/// the test result if it did time out. | ||
/// Unlike <see cref="CancelAfterCommand"/> this command will try to Abort the current executing Thread after the timeout. | ||
/// </summary> | ||
public class TimeoutCommand : BeforeAndAfterTestCommand | ||
public class TimeoutCommand : CancelAfterCommand | ||
{ | ||
private readonly int _timeout; | ||
private readonly IDebugger _debugger; | ||
#if THREAD_ABORT | ||
private Timer? _commandTimer; | ||
private bool _commandTimedOut; | ||
#endif | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="TimeoutCommand"/> class. | ||
/// </summary> | ||
/// <param name="innerCommand">The inner command</param> | ||
/// <param name="timeout">Timeout value</param> | ||
/// <param name="debugger">An <see cref="IDebugger" /> instance</param> | ||
internal TimeoutCommand(TestCommand innerCommand, int timeout, IDebugger debugger) : base(innerCommand) | ||
internal TimeoutCommand(TestCommand innerCommand, int timeout, IDebugger debugger) | ||
: base(innerCommand, timeout, debugger) | ||
{ | ||
_timeout = timeout; | ||
_debugger = debugger; | ||
|
||
Guard.ArgumentValid(innerCommand.Test is TestMethod, "TimeoutCommand may only apply to a TestMethod", nameof(innerCommand)); | ||
Guard.ArgumentValid(timeout > 0, "Timeout value must be greater than zero", nameof(timeout)); | ||
Guard.ArgumentNotNull(debugger, nameof(debugger)); | ||
|
||
#if THREAD_ABORT | ||
BeforeTest = _ => | ||
{ | ||
var testThread = Thread.CurrentThread; | ||
var nativeThreadId = ThreadUtility.GetCurrentThreadNativeId(); | ||
// Create a timer to cancel the current thread | ||
_commandTimer = new Timer( | ||
o => | ||
{ | ||
if (_debugger.IsAttached) | ||
{ | ||
return; | ||
} | ||
_commandTimedOut = true; | ||
ThreadUtility.Abort(testThread, nativeThreadId); | ||
// No join here, since the thread doesn't really terminate | ||
}, | ||
null, | ||
timeout, | ||
Timeout.Infinite); | ||
}; | ||
|
||
AfterTest = (context) => | ||
{ | ||
_commandTimer?.Dispose(); | ||
// If the timer cancelled the current thread, change the result | ||
if (_commandTimedOut) | ||
{ | ||
var message = $"Test exceeded Timeout value of {timeout}ms"; | ||
context.CurrentResult.SetResult( | ||
ResultState.Failure, | ||
message); | ||
} | ||
}; | ||
#else | ||
BeforeTest = _ => { }; | ||
AfterTest = _ => { }; | ||
#endif | ||
} | ||
|
||
#if !THREAD_ABORT | ||
/// <summary> | ||
/// Runs the test, saving a TestResult in the supplied TestExecutionContext. | ||
/// </summary> | ||
/// <param name="context">The context in which the test should run.</param> | ||
/// <returns>A TestResult</returns> | ||
public override TestResult Execute(TestExecutionContext context) | ||
{ | ||
try | ||
{ | ||
var testExecution = RunTestOnSeparateThread(context); | ||
if (Task.WaitAny(new Task[] { testExecution }, _timeout) != -1 | ||
|| _debugger.IsAttached) | ||
{ | ||
context.CurrentResult = testExecution.GetAwaiter().GetResult(); | ||
} | ||
else | ||
{ | ||
string message = $"Test exceeded Timeout value of {_timeout}ms"; | ||
|
||
context.CurrentResult.SetResult( | ||
ResultState.Failure, | ||
message); | ||
} | ||
} | ||
catch (Exception exception) | ||
{ | ||
context.CurrentResult.RecordException(exception, FailureSite.Test); | ||
} | ||
|
||
return context.CurrentResult; | ||
} | ||
|
||
private Task<TestResult> RunTestOnSeparateThread(TestExecutionContext context) | ||
/// <inheritdoc/> | ||
protected override void ExecuteInnerCommand(TestExecutionContext context) | ||
{ | ||
var separateContext = new TestExecutionContext(context) | ||
{ | ||
CurrentResult = context.CurrentTest.MakeTestResult() | ||
}; | ||
return Task.Run(() => innerCommand.Execute(separateContext)); | ||
#pragma warning disable SYSLIB0046 // Type or member is obsolete | ||
ControlledExecution.Run(() => base.ExecuteInnerCommand(context), context.CancellationToken); | ||
#pragma warning restore SYSLIB0046 // Type or member is obsolete | ||
} | ||
#endif | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
src/NUnitFramework/framework/Internal/ControlledExecution.Net6.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt | ||
// | ||
// Adapted from original: | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
#if NET6_0 | ||
|
||
using System.Threading; | ||
|
||
namespace System.Runtime | ||
{ | ||
/// <summary> | ||
/// Allows to run code and abort it asynchronously. | ||
/// </summary> | ||
internal static class ControlledExecution | ||
{ | ||
/// <summary> | ||
/// Runs code that may be aborted asynchronously. | ||
/// </summary> | ||
/// <param name="action">The delegate that represents the code to execute.</param> | ||
/// <param name="cancellationToken">The cancellation token that may be used to abort execution.</param> | ||
/// <exception cref="PlatformNotSupportedException">The method is not supported on this platform.</exception> | ||
/// <exception cref="ArgumentNullException">The <paramref name="action"/> argument is null.</exception> | ||
/// <exception cref="InvalidOperationException"> | ||
/// The current thread is already running the <see cref="Run"/> method. | ||
/// </exception> | ||
/// <exception cref="OperationCanceledException">The execution was aborted.</exception> | ||
/// <remarks> | ||
/// <para>This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception | ||
/// in the thread executing that code. While the exception may be caught by the code, it is re-thrown at the end | ||
/// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method.</para> | ||
/// <para>Execution of the code is not guaranteed to abort immediately, or at all. This situation can occur, for | ||
/// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as | ||
/// part of the abort procedure, thereby indefinitely delaying the abort. Furthermore, execution may not be | ||
/// aborted immediately if the thread is currently executing a `catch` or `finally` block.</para> | ||
/// <para>Aborting code at an unexpected location may corrupt the state of data structures in the process and lead | ||
/// to unpredictable results. For that reason, this method should not be used in production code and calling it | ||
/// produces a compile-time warning.</para> | ||
/// </remarks> | ||
public static void Run(Action action, CancellationToken cancellationToken) | ||
{ | ||
if (action is null) | ||
{ | ||
throw new ArgumentNullException(nameof(action)); | ||
} | ||
|
||
throw new PlatformNotSupportedException( | ||
".NET 6.0 has no Thread.Abort functionality, use cooperative cancellation using the CancelAfterAttribute"); | ||
} | ||
} | ||
} | ||
|
||
#endif |
Oops, something went wrong.