WorkItem
A WorkItem may be an individual test case, a fixture or
a higher level grouping of tests. All WorkItems inherit
from the abstract WorkItem class, which uses the template
pattern to allow derived classes to perform work in
whatever way is needed.
A WorkItem is created with a particular TestExecutionContext
and is responsible for re-establishing that context in the
current thread before it begins or resumes execution.
using NUnit.Framework.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace NUnit.Framework.Internal.Execution
{
public abstract class WorkItem
{
[Flags]
private enum OwnThreadReason
{
NotNeeded = 0,
RequiresThread = 1,
Timeout = 2,
DifferentApartment = 4
}
private static Logger log = InternalTrace.GetLogger("WorkItem");
private Thread thread;
private object threadLock = new object();
public WorkItemState State { get; set; }
public Test Test { get; set; }
public TestExecutionContext Context { get; set; }
public string WorkerId { get; set; }
public List<ITestAction> Actions { get; set; }
public bool IsParallelizable {
get {
ParallelScope parallelScope = ParallelScope.None;
if (Test.Properties.ContainsKey("ParallelScope")) {
parallelScope = (ParallelScope)Test.Properties.Get("ParallelScope");
if ((parallelScope & ParallelScope.Self) != 0)
return true;
} else {
parallelScope = Context.ParallelScope;
if ((parallelScope & ParallelScope.Children) != 0)
return true;
}
if (Test is TestFixture && (parallelScope & ParallelScope.Fixtures) != 0)
return true;
if (Test is TestAssembly && parallelScope != 0)
return true;
return false;
}
}
public TestResult Result { get; set; }
internal ApartmentState TargetApartment { get; set; }
private ApartmentState CurrentApartment { get; set; }
public event EventHandler Completed;
public static WorkItem CreateWorkItem(ITest test, ITestFilter filter)
{
TestSuite testSuite = test as TestSuite;
if (testSuite != null)
return new CompositeWorkItem(testSuite, filter);
return new SimpleWorkItem((TestMethod)test, filter);
}
public WorkItem(Test test)
{
Test = test;
Result = test.MakeTestResult();
State = WorkItemState.Ready;
Actions = new List<ITestAction>();
TargetApartment = (Test.Properties.ContainsKey("ApartmentState") ? ((ApartmentState)Test.Properties.Get("ApartmentState")) : ApartmentState.Unknown);
}
public void InitializeContext(TestExecutionContext context)
{
Guard.OperationValid(Context == null, "The context has already been initialized");
Context = context;
if (Test is TestAssembly)
Actions.AddRange(ActionsHelper.GetActionsFromAttributeProvider(((TestAssembly)Test).Assembly));
else if (Test is ParameterizedMethodSuite) {
Actions.AddRange(ActionsHelper.GetActionsFromAttributeProvider(Test.Method.MethodInfo));
} else if (Test.TypeInfo != null) {
Actions.AddRange(ActionsHelper.GetActionsFromTypesAttributes(Test.TypeInfo.Type));
}
}
public virtual void Execute()
{
int num = Context.TestCaseTimeout;
if (Test.Properties.ContainsKey("Timeout"))
num = (int)Test.Properties.Get("Timeout");
OwnThreadReason ownThreadReason = OwnThreadReason.NotNeeded;
if (Test.RequiresThread)
ownThreadReason |= OwnThreadReason.RequiresThread;
if (num > 0 && Test is TestMethod)
ownThreadReason |= OwnThreadReason.Timeout;
CurrentApartment = Thread.CurrentThread.GetApartmentState();
if (CurrentApartment != TargetApartment && TargetApartment != ApartmentState.Unknown)
ownThreadReason |= OwnThreadReason.DifferentApartment;
if (ownThreadReason == OwnThreadReason.NotNeeded)
RunTest();
else if (Context.IsSingleThreaded) {
string message = "Test is not runnable in single-threaded context. " + ownThreadReason;
log.Error(message);
Result.SetResult(ResultState.NotRunnable, message);
WorkItemComplete();
} else {
log.Debug("Running test on own thread. " + ownThreadReason);
ApartmentState apartment = ((ownThreadReason | OwnThreadReason.DifferentApartment) != 0) ? TargetApartment : CurrentApartment;
RunTestOnOwnThread(num, apartment);
}
}
private void RunTestOnOwnThread(int timeout, ApartmentState apartment)
{
thread = new Thread(RunTest);
thread.SetApartmentState(apartment);
RunThread(timeout);
}
private void RunThread(int timeout)
{
this.thread.CurrentCulture = Context.CurrentCulture;
this.thread.CurrentUICulture = Context.CurrentUICulture;
this.thread.Start();
if (timeout <= 0)
timeout = -1;
if (!this.thread.Join(timeout)) {
if (Debugger.IsAttached)
this.thread.Join();
else {
Thread thread = default(Thread);
lock (threadLock) {
if (this.thread == null)
return;
thread = this.thread;
this.thread = null;
}
if (Context.ExecutionStatus != TestExecutionStatus.AbortRequested) {
log.Debug("Killing thread {0}, which exceeded timeout", thread.ManagedThreadId);
ThreadUtility.Kill(thread);
thread.Join();
log.Debug("Changing result from {0} to Timeout Failure", Result.ResultState);
Result.SetResult(ResultState.Failure, $"""{timeout}""");
WorkItemComplete();
}
}
}
}
private void RunTest()
{
Context.CurrentTest = Test;
Context.CurrentResult = Result;
Context.Listener.TestStarted(Test);
Context.StartTime = DateTime.UtcNow;
Context.StartTicks = Stopwatch.GetTimestamp();
Context.WorkerId = WorkerId;
Context.EstablishExecutionEnvironment();
State = WorkItemState.Running;
PerformWork();
}
public virtual void Cancel(bool force)
{
if (Context != null)
Context.ExecutionStatus = ((!force) ? TestExecutionStatus.StopRequested : TestExecutionStatus.AbortRequested);
if (force) {
Thread thread = default(Thread);
lock (threadLock) {
if (this.thread == null)
return;
thread = this.thread;
this.thread = null;
}
if (!thread.Join(0)) {
log.Debug("Killing thread {0} for cancel", thread.ManagedThreadId);
ThreadUtility.Kill(thread);
thread.Join();
log.Debug("Changing result from {0} to Cancelled", Result.ResultState);
Result.SetResult(ResultState.Cancelled, "Cancelled by user");
WorkItemComplete();
}
}
}
protected abstract void PerformWork();
protected void WorkItemComplete()
{
State = WorkItemState.Complete;
Result.StartTime = Context.StartTime;
Result.EndTime = DateTime.UtcNow;
double duration = (double)(Stopwatch.GetTimestamp() - Context.StartTicks) / (double)Stopwatch.Frequency;
Result.Duration = duration;
Result.AssertCount += Context.AssertCount;
Context.Listener.TestFinished(Result);
if (this.Completed != null)
this.Completed(this, EventArgs.Empty);
Context.TestObject = null;
Test.Fixture = null;
}
}
}