-
Notifications
You must be signed in to change notification settings - Fork 292
Expand file tree
/
Copy pathAssemblyEnumerator.cs
More file actions
383 lines (333 loc) · 17.3 KB
/
AssemblyEnumerator.cs
File metadata and controls
383 lines (333 loc) · 17.3 KB
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Runtime.Serialization;
using System.Security;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.Internal;
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
/// <summary>
/// Enumerates through all types in the assembly in search of valid test methods.
/// </summary>
[SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for testability")]
internal class AssemblyEnumerator : MarshalByRefObject
{
/// <summary>
/// Helper for reflection API's.
/// </summary>
private static readonly ReflectHelper ReflectHelper = ReflectHelper.Instance;
/// <summary>
/// Type cache.
/// </summary>
private readonly TypeCache _typeCache = new(ReflectHelper);
/// <summary>
/// Initializes a new instance of the <see cref="AssemblyEnumerator"/> class.
/// </summary>
public AssemblyEnumerator()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AssemblyEnumerator"/> class.
/// </summary>
/// <param name="settings">The settings for the session.</param>
/// <remarks>Use this constructor when creating this object in a new app domain so the settings for this app domain are set.</remarks>
public AssemblyEnumerator(MSTestSettings settings) =>
// Populate the settings into the domain(Desktop workflow) performing discovery.
// This would just be resetting the settings to itself in non desktop workflows.
MSTestSettings.PopulateSettings(settings);
/// <summary>
/// Returns object to be used for controlling lifetime, null means infinite lifetime.
/// </summary>
/// <returns>
/// The <see cref="object"/>.
/// </returns>
[SecurityCritical]
#if NET5_0_OR_GREATER
[Obsolete]
#endif
public override object InitializeLifetimeService() => null!;
/// <summary>
/// Enumerates through all types in the assembly in search of valid test methods.
/// </summary>
/// <param name="assemblyFileName">The assembly file name.</param>
/// <param name="mustSerialize">Flag set to true when parameterized test data must be serialized.</param>
/// <returns>A collection of Test Elements.</returns>
internal AssemblyEnumerationResult EnumerateAssembly(string assemblyFileName, bool mustSerialize)
{
List<string> warnings = [];
DebugEx.Assert(!StringEx.IsNullOrWhiteSpace(assemblyFileName), "Invalid assembly file name.");
var tests = new List<UnitTestElement>();
Assembly assembly = PlatformServiceProvider.Instance.FileOperations.LoadAssembly(assemblyFileName);
Type[] types = GetTypes(assembly);
bool discoverInternals = ReflectHelper.HasDiscoverInternalsAttribute(assembly);
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy = ReflectHelper.GetTestDataSourceOptions(assembly)?.UnfoldingStrategy switch
{
// When strategy is auto we want to unfold
TestDataSourceUnfoldingStrategy.Auto => TestDataSourceUnfoldingStrategy.Unfold,
// When strategy is set, let's use it
{ } value => value,
// When the attribute is not set, let's look at the legacy attribute
null => ReflectHelper.GetTestDataSourceDiscoveryOption(assembly) switch
{
TestDataSourceDiscoveryOption.DuringExecution => TestDataSourceUnfoldingStrategy.Fold,
_ => TestDataSourceUnfoldingStrategy.Unfold,
},
};
foreach (Type type in types)
{
List<UnitTestElement> testsInType = DiscoverTestsInType(assemblyFileName, type, warnings, discoverInternals,
dataSourcesUnfoldingStrategy, mustSerialize);
tests.AddRange(testsInType);
}
return new AssemblyEnumerationResult(tests, warnings);
}
/// <summary>
/// Gets the types defined in an assembly.
/// </summary>
/// <param name="assembly">The reflected assembly.</param>
/// <returns>Gets the types defined in the provided assembly.</returns>
internal static Type[] GetTypes(Assembly assembly)
{
try
{
return PlatformServiceProvider.Instance.ReflectionOperations.GetDefinedTypes(assembly);
}
catch (ReflectionTypeLoadException ex)
{
if (ex.LoaderExceptions != null)
{
if (ex.LoaderExceptions.Length == 1 && ex.LoaderExceptions[0] is { } singleLoaderException)
{
// This exception might be more clear than the ReflectionTypeLoadException, so we throw it.
throw singleLoaderException;
}
// If we have multiple loader exceptions, we log them all as errors, and then throw the original exception.
foreach (Exception? loaderEx in ex.LoaderExceptions)
{
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsErrorEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Error("{0}", loaderEx);
}
}
}
throw;
}
}
/// <summary>
/// Returns an instance of the <see cref="TypeEnumerator"/> class.
/// </summary>
/// <param name="type">The type to enumerate.</param>
/// <param name="assemblyFileName">The reflected assembly name.</param>
/// <param name="discoverInternals">True to discover test classes which are declared internal in
/// addition to test classes which are declared public.</param>
/// <returns>a TypeEnumerator instance.</returns>
internal virtual TypeEnumerator GetTypeEnumerator(Type type, string assemblyFileName, bool discoverInternals)
{
var typeValidator = new TypeValidator(ReflectHelper, discoverInternals);
var testMethodValidator = new TestMethodValidator(ReflectHelper, discoverInternals);
#if !WINDOWS_UWP && !WIN_UI
return new TypeEnumerator(type, assemblyFileName, ReflectHelper, typeValidator, testMethodValidator, Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestTelemetryDataCollector.Current);
#else
return new TypeEnumerator(type, assemblyFileName, ReflectHelper, typeValidator, testMethodValidator);
#endif
}
private List<UnitTestElement> DiscoverTestsInType(
string assemblyFileName,
Type type,
List<string> warningMessages,
bool discoverInternals,
TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy,
bool mustSerialize)
{
string? typeFullName = null;
var tests = new List<UnitTestElement>();
try
{
typeFullName = type.FullName;
TypeEnumerator testTypeEnumerator = GetTypeEnumerator(type, assemblyFileName, discoverInternals);
List<UnitTestElement>? unitTestCases = testTypeEnumerator.Enumerate(warningMessages);
if (unitTestCases != null)
{
foreach (UnitTestElement test in unitTestCases)
{
if (_typeCache.GetTestMethodInfoForDiscovery(test.TestMethod) is { } testMethodInfo)
{
if (TryUnfoldITestDataSources(test, testMethodInfo, dataSourcesUnfoldingStrategy, tests, mustSerialize))
{
continue;
}
}
tests.Add(test);
}
}
}
catch (Exception exception)
{
// If we fail to discover type from a class, then don't abort the discovery
// Move to the next type.
string message = string.Format(CultureInfo.CurrentCulture, Resource.CouldNotInspectTypeDuringDiscovery, typeFullName, assemblyFileName, exception.Message);
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info($"AssemblyEnumerator: {message}");
}
warningMessages.Add(message);
}
return tests;
}
private static bool TryUnfoldITestDataSources(UnitTestElement test, DiscoveryTestMethodInfo testMethodInfo, TestDataSourceUnfoldingStrategy dataSourcesUnfoldingStrategy, List<UnitTestElement> tests, bool mustSerialize)
{
// It should always be `true`, but if any part of the chain is obsolete; it might not contain those.
// Since we depend on those properties, if they don't exist, we bail out early.
if (!test.TestMethod.HasManagedMethodAndTypeProperties)
{
return false;
}
// If the global strategy is to fold and local uses Auto then return false
if (dataSourcesUnfoldingStrategy == TestDataSourceUnfoldingStrategy.Fold
&& test.UnfoldingStrategy == TestDataSourceUnfoldingStrategy.Auto)
{
return false;
}
// If the data source specifies the unfolding strategy as fold then return false
if (test.UnfoldingStrategy == TestDataSourceUnfoldingStrategy.Fold)
{
return false;
}
// We don't have a special method to filter attributes that are not derived from Attribute, so we take all
// attributes and filter them. We don't have to care if there is one, because this method is only entered when
// there is at least one (we determine this in TypeEnumerator.GetTestFromMethod.
IEnumerable<ITestDataSource> testDataSources = ReflectHelper.Instance.GetAttributes<Attribute>(testMethodInfo.MethodInfo).OfType<ITestDataSource>();
// We need to use a temporary list to avoid adding tests to the main list if we fail to expand any data source.
List<UnitTestElement> tempListOfTests = [];
try
{
bool isDataDriven = false;
int globalTestCaseIndex = 0;
foreach (ITestDataSource dataSource in testDataSources)
{
isDataDriven = true;
if (!TryUnfoldITestDataSource(dataSource, test, new(testMethodInfo.MethodInfo, test.TestMethod.DisplayName), tempListOfTests, ref globalTestCaseIndex, mustSerialize))
{
// TODO: Improve multi-source design!
// Ideally we would want to consider each data source separately but when one source cannot be expanded,
// we will run all sources from the given method so we need to bail-out "globally".
return false;
}
}
if (tempListOfTests.Count > 0)
{
tests.AddRange(tempListOfTests);
}
return isDataDriven;
}
catch (Exception ex)
{
string message = string.Format(CultureInfo.CurrentCulture, Resource.CannotEnumerateIDataSourceAttribute, test.TestMethod.ManagedTypeName, test.TestMethod.ManagedMethodName, ex);
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info($"DynamicDataEnumerator: {message}");
}
if (tempListOfTests.Count > 0)
{
tests.AddRange(tempListOfTests);
}
return false;
}
}
private static bool TryUnfoldITestDataSource(ITestDataSource dataSource, UnitTestElement test, ReflectionTestMethodInfo methodInfo, List<UnitTestElement> tests, ref int globalTestCaseIndex, bool mustSerialize)
{
// Otherwise, unfold the data source and verify it can be serialized.
// This code is to discover tests. To run the tests code is in TestMethodRunner.TryExecuteFoldedDataDrivenTestsAsync.
// Any change made here should be reflected in TestMethodRunner.TryExecuteFoldedDataDrivenTestsAsync as well.
IEnumerable<object?[]> dataEnumerable = dataSource.GetData(methodInfo);
string? testDataSourceIgnoreMessage = (dataSource as ITestDataSourceIgnoreCapability)?.IgnoreMessage;
var discoveredTests = new List<UnitTestElement>();
bool dataSourceHasData = false;
foreach (object?[] dataOrTestDataRow in dataEnumerable)
{
dataSourceHasData = true;
object?[] d = dataOrTestDataRow;
ParameterInfo[] parameters = methodInfo.GetParameters();
if (TestDataSourceHelpers.TryHandleITestDataRow(d, parameters, out d, out string? ignoreMessageFromTestDataRow, out string? displayNameFromTestDataRow, out IList<string>? testCategoriesFromTestDataRow))
{
testDataSourceIgnoreMessage = ignoreMessageFromTestDataRow ?? testDataSourceIgnoreMessage;
}
else if (TestDataSourceHelpers.IsDataConsideredSingleArgumentValue(d, parameters))
{
// SPECIAL CASE:
// This condition is a duplicate of the condition in GetInvokeResultAsync.
//
// The known scenario we know of that shows importance of that check is if we have DynamicData using this member
//
// public static IEnumerable<object[]> GetData()
// {
// yield return new object[] { ("Hello", "World") };
// }
//
// If the test method has a single parameter which is 'object[]', then we should pass the tuple array as is.
// Note that normally, the array in this code path represents the arguments of the test method.
// However, GetInvokeResultAsync uses the above check to mean "the whole array is the single argument to the test method"
}
else if (d?.Length == 1 && TestDataSourceHelpers.TryHandleTupleDataSource(d[0], parameters, out object?[] tupleExpandedToArray))
{
d = tupleExpandedToArray;
}
UnitTestElement discoveredTest = test.Clone();
discoveredTest.TestMethod.DisplayName = displayNameFromTestDataRow
?? dataSource.GetDisplayName(methodInfo, d)
?? TestDataSourceUtilities.ComputeDefaultDisplayName(methodInfo, d)
?? discoveredTest.TestMethod.DisplayName;
// Merge test categories from the test data row with the existing categories
if (testCategoriesFromTestDataRow is { Count: > 0 })
{
discoveredTest.TestCategory = discoveredTest.TestCategory is { Length: > 0 }
? [.. testCategoriesFromTestDataRow, .. discoveredTest.TestCategory]
: [.. testCategoriesFromTestDataRow];
}
try
{
if (mustSerialize)
{
discoveredTest.TestMethod.SerializedData = DataSerializationHelper.Serialize(d);
}
discoveredTest.TestMethod.ActualData = d;
discoveredTest.TestMethod.TestCaseIndex = globalTestCaseIndex;
discoveredTest.TestMethod.TestDataSourceIgnoreMessage = testDataSourceIgnoreMessage;
discoveredTest.TestMethod.DataType = DynamicDataType.ITestDataSource;
}
catch (SerializationException ex)
{
string warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute_CannotSerialize, globalTestCaseIndex, discoveredTest.TestMethod.DisplayName);
warning += Environment.NewLine;
warning += ex.ToString();
warning = string.Format(CultureInfo.CurrentCulture, Resource.CannotExpandIDataSourceAttribute, test.TestMethod.ManagedTypeName, test.TestMethod.ManagedMethodName, warning);
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Warning($"DynamicDataEnumerator: {warning}");
}
// Serialization failed for the type, bail out. Caller will handle adding the original test.
return false;
}
discoveredTests.Add(discoveredTest);
globalTestCaseIndex++;
}
if (!dataSourceHasData)
{
if (!MSTestSettings.CurrentSettings.ConsiderEmptyDataSourceAsInconclusive)
{
throw dataSource.GetExceptionForEmptyDataSource(methodInfo);
}
UnitTestElement discoveredTest = test.Clone();
// Make the test not data driven, because it had no data.
discoveredTest.TestMethod.DataType = DynamicDataType.None;
discoveredTest.TestMethod.TestDataSourceIgnoreMessage = testDataSourceIgnoreMessage;
discoveredTest.TestMethod.DisplayName = dataSource.GetDisplayName(methodInfo, null) ?? discoveredTest.TestMethod.DisplayName;
tests.Add(discoveredTest);
return true;
}
tests.AddRange(discoveredTests);
return true;
}
}