-
Notifications
You must be signed in to change notification settings - Fork 758
/
TestAssemblyRunner.cs
261 lines (227 loc) · 11.8 KB
/
TestAssemblyRunner.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
namespace Xunit.Sdk
{
/// <summary>
/// A base class that provides default behavior when running tests in an assembly. It groups the tests
/// by test collection, and then runs the individual test collections.
/// </summary>
/// <typeparam name="TTestCase">The type of the test case used by the test framework. Must
/// derive from <see cref="ITestCase"/>.</typeparam>
public abstract class TestAssemblyRunner<TTestCase> : IDisposable
where TTestCase : ITestCase
{
/// <summary>
/// Initializes a new instance of the <see cref="TestAssemblyRunner{TTestCase}"/> class.
/// </summary>
/// <param name="testAssembly">The assembly that contains the tests to be run.</param>
/// <param name="testCases">The test cases to be run.</param>
/// <param name="diagnosticMessageSink">The message sink to report diagnostic messages to.</param>
/// <param name="executionMessageSink">The message sink to report run status to.</param>
/// <param name="executionOptions">The user's requested execution options.</param>
protected TestAssemblyRunner(ITestAssembly testAssembly,
IEnumerable<TTestCase> testCases,
IMessageSink diagnosticMessageSink,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
TestAssembly = testAssembly;
TestCases = testCases;
DiagnosticMessageSink = diagnosticMessageSink;
ExecutionMessageSink = executionMessageSink;
ExecutionOptions = executionOptions;
TestCaseOrderer = new DefaultTestCaseOrderer(diagnosticMessageSink);
}
/// <summary>
/// Gets or sets the exception aggregator used to run code and collect exceptions.
/// </summary>
protected ExceptionAggregator Aggregator { get; set; } = new ExceptionAggregator();
/// <summary>
/// Gets or sets the user's requested execution options.
/// </summary>
protected ITestFrameworkExecutionOptions ExecutionOptions { get; set; }
/// <summary>
/// Gets or sets the message sink to report diagnostic messages to.
/// </summary>
protected IMessageSink DiagnosticMessageSink { get; set; }
/// <summary>
/// Gets or sets the message sink to report run status to.
/// </summary>
protected IMessageSink ExecutionMessageSink { get; set; }
/// <summary>
/// Gets or sets the assembly that contains the tests to be run.
/// </summary>
protected ITestAssembly TestAssembly { get; set; }
/// <summary>
/// Gets or sets the test case orderer that will be used to decide how to order the tests.
/// </summary>
protected ITestCaseOrderer TestCaseOrderer { get; set; }
/// <summary>
/// Gets or sets the test collection orderer that will be used to decide how to order the test collections.
/// </summary>
protected ITestCollectionOrderer TestCollectionOrderer { get; set; } = new DefaultTestCollectionOrderer();
/// <summary>
/// Gets or sets the test cases to be run.
/// </summary>
protected IEnumerable<TTestCase> TestCases { get; set; }
/// <inheritdoc/>
public virtual void Dispose() { }
/// <summary>
/// Override this to provide the display name for the test framework (f.e., "xUnit.net 2.0").
/// This value is placed into <see cref="ITestAssemblyStarting.TestFrameworkDisplayName"/>.
/// </summary>
protected abstract string GetTestFrameworkDisplayName();
/// <summary>
/// Override this to provide the environment information (f.e., "32-bit .NET 4.0"). This value is
/// placed into <see cref="ITestAssemblyStarting.TestEnvironment"/>.
/// </summary>
protected virtual string GetTestFrameworkEnvironment()
=> string.Format(CultureInfo.CurrentCulture, "{0}-bit .NET {1}", IntPtr.Size * 8, GetVersion());
static string GetVersion()
{
var attr = typeof(object).GetTypeInfo().Assembly.GetCustomAttribute<TargetFrameworkAttribute>();
if (attr != null)
return attr.FrameworkDisplayName;
#if NETFRAMEWORK
return Environment.Version.ToString();
#else
return "Standard";
#endif
}
/// <summary>
/// This method is called just after <see cref="ITestAssemblyStarting"/> is sent, but before any test collections are run.
/// This method should NEVER throw; any exceptions should be placed into the <see cref="Aggregator"/>.
/// </summary>
protected virtual Task AfterTestAssemblyStartingAsync()
=> CommonTasks.Completed;
/// <summary>
/// This method is called just before <see cref="ITestAssemblyFinished"/> is sent.
/// This method should NEVER throw; any exceptions should be placed into the <see cref="Aggregator"/>.
/// </summary>
protected virtual Task BeforeTestAssemblyFinishedAsync()
=> CommonTasks.Completed;
/// <summary>
/// Creates the message bus to be used for test execution. By default, it inspects
/// the options for the <see cref="TestOptionsNames.Execution.SynchronousMessageReporting"/>
/// flag, and if present, creates a message bus that ensures all messages are delivered
/// on the same thread.
/// </summary>
/// <returns>The message bus.</returns>
protected virtual IMessageBus CreateMessageBus()
{
if (ExecutionOptions.SynchronousMessageReportingOrDefault())
return new SynchronousMessageBus(ExecutionMessageSink);
return new MessageBus(ExecutionMessageSink, ExecutionOptions.StopOnTestFailOrDefault());
}
/// <summary>
/// Orders the test collections using the <see cref="TestCollectionOrderer"/>.
/// </summary>
/// <returns>Test collections (and the associated test cases) in run order</returns>
protected List<Tuple<ITestCollection, List<TTestCase>>> OrderTestCollections()
{
var testCasesByCollection =
TestCases.GroupBy(tc => tc.TestMethod.TestClass.TestCollection, TestCollectionComparer.Instance)
.ToDictionary(collectionGroup => collectionGroup.Key, collectionGroup => collectionGroup.ToList());
IEnumerable<ITestCollection> orderedTestCollections;
try
{
orderedTestCollections = TestCollectionOrderer.OrderTestCollections(testCasesByCollection.Keys);
}
catch (Exception ex)
{
var innerEx = ex.Unwrap();
DiagnosticMessageSink.OnMessage(
new DiagnosticMessage(
"Test collection orderer '{0}' threw '{1}' during ordering: {2}{3}{4}",
TestCollectionOrderer.GetType().FullName,
innerEx.GetType().FullName,
innerEx.Message,
Environment.NewLine,
innerEx.StackTrace
)
);
orderedTestCollections = testCasesByCollection.Keys.ToList();
}
return orderedTestCollections.Select(collection => Tuple.Create(collection, testCasesByCollection[collection]))
.ToList();
}
/// <summary>
/// Runs the tests in the test assembly.
/// </summary>
/// <returns>Returns summary information about the tests that were run.</returns>
public async Task<RunSummary> RunAsync()
{
var cancellationTokenSource = new CancellationTokenSource();
var totalSummary = new RunSummary();
#if NETFRAMEWORK
var currentDirectory = Directory.GetCurrentDirectory();
#endif
var testFrameworkEnvironment = GetTestFrameworkEnvironment();
var testFrameworkDisplayName = GetTestFrameworkDisplayName();
using (var messageBus = CreateMessageBus())
{
#if NETFRAMEWORK
Directory.SetCurrentDirectory(Path.GetDirectoryName(TestAssembly.Assembly.AssemblyPath));
#endif
if (messageBus.QueueMessage(new TestAssemblyStarting(TestCases.Cast<ITestCase>(), TestAssembly, DateTime.Now, testFrameworkEnvironment, testFrameworkDisplayName)))
{
try
{
await AfterTestAssemblyStartingAsync();
var masterStopwatch = Stopwatch.StartNew();
totalSummary = await RunTestCollectionsAsync(messageBus, cancellationTokenSource);
// Want clock time, not aggregated run time
totalSummary.Time = (decimal)masterStopwatch.Elapsed.TotalSeconds;
Aggregator.Clear();
await BeforeTestAssemblyFinishedAsync();
if (Aggregator.HasExceptions)
messageBus.QueueMessage(new TestAssemblyCleanupFailure(TestCases.Cast<ITestCase>(), TestAssembly, Aggregator.ToException()));
}
finally
{
messageBus.QueueMessage(new TestAssemblyFinished(TestCases.Cast<ITestCase>(), TestAssembly, totalSummary.Time, totalSummary.Total, totalSummary.Failed, totalSummary.Skipped));
#if NETFRAMEWORK
Directory.SetCurrentDirectory(currentDirectory);
#endif
}
}
}
return totalSummary;
}
/// <summary>
/// Runs the list of test collections. By default, groups the tests by collection and runs them synchronously.
/// </summary>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
/// <returns>Returns summary information about the tests that were run.</returns>
protected virtual async Task<RunSummary> RunTestCollectionsAsync(IMessageBus messageBus, CancellationTokenSource cancellationTokenSource)
{
var summary = new RunSummary();
foreach (var collection in OrderTestCollections())
{
summary.Aggregate(await RunTestCollectionAsync(messageBus, collection.Item1, collection.Item2, cancellationTokenSource));
if (cancellationTokenSource.IsCancellationRequested)
break;
}
return summary;
}
/// <summary>
/// Override this method to run the tests in an individual test collection.
/// </summary>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="testCollection">The test collection that is being run.</param>
/// <param name="testCases">The test cases to be run.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
/// <returns>Returns summary information about the tests that were run.</returns>
protected abstract Task<RunSummary> RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable<TTestCase> testCases, CancellationTokenSource cancellationTokenSource);
}
}