forked from microsoft/testfx
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReflectHelper.cs
More file actions
356 lines (312 loc) · 16.6 KB
/
ReflectHelper.cs
File metadata and controls
356 lines (312 loc) · 16.6 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
// 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.Security;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
[SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for mocking")]
internal class ReflectHelper : MarshalByRefObject
{
#pragma warning disable RS0030 // Do not use banned APIs
private static readonly Lazy<ReflectHelper> InstanceValue = new(() => new());
#pragma warning restore RS0030 // Do not use banned APIs
// PERF: This was moved from Dictionary<MemberInfo, Dictionary<string, object>> to Concurrent<ICustomAttributeProvider, Attribute[]>
// storing an array allows us to store multiple attributes of the same type if we find them. It also has lower memory footprint, and is faster
// when we are going through the whole collection. Giving us overall better perf.
private readonly ConcurrentDictionary<ICustomAttributeProvider, Attribute[]> _attributeCache = [];
public static ReflectHelper Instance => InstanceValue.Value;
/// <summary>
/// Checks to see if a member or type is decorated with the given attribute, or an attribute that derives from it. e.g. [MyTestClass] from [TestClass] will match if you look for [TestClass]. The inherit parameter does not impact this checking.
/// </summary>
/// <typeparam name="TAttribute">Attribute to search for.</typeparam>
/// <param name="memberInfo">Member to inspect for attributes.</param>
/// <returns>True if the attribute of the specified type is defined on this member or a class.</returns>
public virtual /* for testing */ bool IsAttributeDefined<TAttribute>(MemberInfo memberInfo)
where TAttribute : Attribute
{
Ensure.NotNull(memberInfo);
// Get all attributes on the member.
Attribute[] attributes = GetCustomAttributesCached(memberInfo);
// Try to find the attribute that is derived from baseAttrType.
foreach (Attribute attribute in attributes)
{
DebugEx.Assert(attribute != null, $"{nameof(ReflectHelper)}.{nameof(GetCustomAttributesCached)}: internal error: wrong value in the attributes dictionary.");
if (attribute is TAttribute)
{
return true;
}
}
return false;
}
/// <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>
/// Gets first attribute that matches the type or is derived from it.
/// Use this together with attribute that does not allow multiple. In such case there cannot be more attributes, and this will avoid the cost of
/// checking for more than one attribute.
/// </summary>
/// <typeparam name="TAttribute">Type of the attribute to find.</typeparam>
/// <param name="attributeProvider">The type, assembly or method.</param>
/// <returns>The attribute that is found or null.</returns>
/// <exception cref="InvalidOperationException">Throws when multiple attributes are found (the attribute must allow multiple).</exception>
public virtual /* for tests, for moq */ TAttribute? GetFirstAttributeOrDefault<TAttribute>(ICustomAttributeProvider attributeProvider)
where TAttribute : Attribute
{
Attribute[] cachedAttributes = GetCustomAttributesCached(attributeProvider);
foreach (Attribute cachedAttribute in cachedAttributes)
{
if (cachedAttribute is TAttribute cachedAttributeAsTAttribute)
{
return cachedAttributeAsTAttribute;
}
}
return null;
}
/// <summary>
/// Match return type of method.
/// </summary>
/// <param name="method">The method to inspect.</param>
/// <param name="returnType">The return type to match.</param>
/// <returns>True if there is a match.</returns>
internal static bool MatchReturnType(MethodInfo method, Type returnType)
{
Ensure.NotNull(method);
Ensure.NotNull(returnType);
return method.ReturnType.Equals(returnType);
}
/// <summary>
/// Returns true when the method is declared in the assembly where the type is declared.
/// </summary>
/// <param name="method">The method to check for.</param>
/// <param name="type">The type declared in the assembly to check.</param>
/// <returns>True if the method is declared in the assembly where the type is declared.</returns>
internal virtual bool IsMethodDeclaredInSameAssemblyAsType(MethodInfo method, Type type)
=> method.DeclaringType!.Assembly.Equals(type.Assembly); // TODO: Investigate if we rely on NRE
/// <summary>
/// Get categories applied to the test method.
/// </summary>
/// <param name="categoryAttributeProvider">The member to inspect.</param>
/// <param name="owningType">The reflected type that owns <paramref name="categoryAttributeProvider"/>.</param>
/// <returns>Categories defined.</returns>
internal virtual /* for tests, we are mocking this */ string[] GetTestCategories(MemberInfo categoryAttributeProvider, Type owningType)
{
IEnumerable<TestCategoryBaseAttribute> methodCategories = GetAttributes<TestCategoryBaseAttribute>(categoryAttributeProvider);
IEnumerable<TestCategoryBaseAttribute> typeCategories = GetAttributes<TestCategoryBaseAttribute>(owningType);
IEnumerable<TestCategoryBaseAttribute> assemblyCategories = GetAttributes<TestCategoryBaseAttribute>(owningType.Assembly);
return [.. methodCategories.Concat(typeCategories).Concat(assemblyCategories).SelectMany(c => c.TestCategories)];
}
/// <summary>
/// Gets the parallelization level set on an assembly.
/// </summary>
/// <param name="assembly"> The test assembly. </param>
/// <returns> The parallelization level if set. -1 otherwise. </returns>
internal static ParallelizeAttribute? GetParallelizeAttribute(Assembly assembly)
=> PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(assembly, typeof(ParallelizeAttribute))
.OfType<ParallelizeAttribute>()
.FirstOrDefault();
/// <summary>
/// Gets discover internals assembly level attribute.
/// </summary>
/// <param name="assembly"> The test assembly. </param>
internal static DiscoverInternalsAttribute? GetDiscoverInternalsAttribute(Assembly assembly)
=> PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(assembly, typeof(DiscoverInternalsAttribute))
.OfType<DiscoverInternalsAttribute>()
.FirstOrDefault();
/// <summary>
/// Gets TestDataSourceDiscovery assembly level attribute.
/// </summary>
/// <param name="assembly"> The test assembly. </param>
internal static TestDataSourceDiscoveryOption? GetTestDataSourceDiscoveryOption(Assembly assembly)
=> PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(assembly, typeof(TestDataSourceDiscoveryAttribute))
.OfType<TestDataSourceDiscoveryAttribute>()
.FirstOrDefault()?.DiscoveryOption;
/// <summary>
/// Gets TestDataSourceOptions assembly level attribute.
/// </summary>
/// <param name="assembly"> The test assembly. </param>
/// <returns> The TestDataSourceOptionsAttribute if set. Null otherwise. </returns>
internal static TestDataSourceOptionsAttribute? GetTestDataSourceOptions(Assembly assembly)
=> PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(assembly, typeof(TestDataSourceOptionsAttribute))
.OfType<TestDataSourceOptionsAttribute>()
.FirstOrDefault();
/// <summary>
/// Get the parallelization behavior for a test method.
/// </summary>
/// <param name="testMethod">Test method.</param>
/// <param name="owningType">The type that owns <paramref name="testMethod"/>.</param>
/// <returns>True if test method should not run in parallel.</returns>
internal bool IsDoNotParallelizeSet(MemberInfo testMethod, Type owningType)
=> IsAttributeDefined<DoNotParallelizeAttribute>(testMethod)
|| IsAttributeDefined<DoNotParallelizeAttribute>(owningType);
/// <summary>
/// Get the parallelization behavior for a test assembly.
/// </summary>
/// <param name="assembly">The test assembly.</param>
/// <returns>True if test assembly should not run in parallel.</returns>
internal static bool IsDoNotParallelizeSet(Assembly assembly)
=> PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(assembly, typeof(DoNotParallelizeAttribute))
.Length != 0;
/// <summary>
/// Priority if any set for test method. Will return priority if attribute is applied to TestMethod
/// else null.
/// </summary>
/// <param name="priorityAttributeProvider">The member to inspect.</param>
/// <returns>Priority value if defined. Null otherwise.</returns>
internal virtual int? GetPriority(MemberInfo priorityAttributeProvider) =>
GetFirstAttributeOrDefault<PriorityAttribute>(priorityAttributeProvider)?.Priority;
/// <summary>
/// KeyValue pairs that are provided by TestPropertyAttributes of the given test method.
/// </summary>
/// <param name="testPropertyProvider">The member to inspect.</param>
/// <returns>List of traits.</returns>
internal Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider)
{
Attribute[] attributesFromMethod = GetCustomAttributesCached(testPropertyProvider);
Attribute[] attributesFromClass = testPropertyProvider.ReflectedType is { } testClass ? GetCustomAttributesCached(testClass) : [];
int countTestPropertyAttribute = 0;
foreach (Attribute attribute in attributesFromMethod)
{
if (attribute is TestPropertyAttribute)
{
countTestPropertyAttribute++;
}
}
foreach (Attribute attribute in attributesFromClass)
{
if (attribute is TestPropertyAttribute)
{
countTestPropertyAttribute++;
}
}
if (countTestPropertyAttribute == 0)
{
// This is the common case that we optimize for. This method used to be an iterator (uses yield return) which is allocating unnecessarily in common cases.
return [];
}
var traits = new Trait[countTestPropertyAttribute];
int index = 0;
foreach (Attribute attribute in attributesFromMethod)
{
if (attribute is TestPropertyAttribute testProperty)
{
traits[index++] = new Trait(testProperty.Name, testProperty.Value);
}
}
foreach (Attribute attribute in attributesFromClass)
{
if (attribute is TestPropertyAttribute testProperty)
{
traits[index++] = new Trait(testProperty.Name, testProperty.Value);
}
}
return traits;
}
/// <summary>
/// Get attribute defined on a method which is of given type of subtype of given type.
/// </summary>
/// <typeparam name="TAttributeType">The attribute type.</typeparam>
/// <param name="attributeProvider">The member to inspect.</param>
/// <returns>An instance of the attribute.</returns>
internal virtual /* for tests, for moq */ IEnumerable<TAttributeType> GetAttributes<TAttributeType>(ICustomAttributeProvider attributeProvider)
where TAttributeType : Attribute
{
Attribute[] attributes = GetCustomAttributesCached(attributeProvider);
// Try to find the attribute that is derived from baseAttrType.
foreach (Attribute attribute in attributes)
{
DebugEx.Assert(attribute != null, "ReflectHelper.DefinesAttributeDerivedFrom: internal error: wrong value in the attributes dictionary.");
if (attribute is TAttributeType attributeAsAttributeType)
{
yield return attributeAsAttributeType;
}
}
}
/// <summary>
/// Get attribute defined on a method which is of given type of subtype of given type.
/// </summary>
/// <typeparam name="TAttributeType">The attribute type.</typeparam>
/// <typeparam name="TState">The type of state to be passed to Action.</typeparam>
/// <param name="attributeProvider">The member to inspect.</param>
/// <param name="action">The action to perform.</param>
/// <param name="state">The state to pass to action.</param>
internal void PerformActionOnAttribute<TAttributeType, TState>(ICustomAttributeProvider attributeProvider, Action<TAttributeType, TState?> action, TState? state)
where TAttributeType : Attribute
{
Attribute[] attributes = GetCustomAttributesCached(attributeProvider);
foreach (Attribute attribute in attributes)
{
DebugEx.Assert(attribute != null, "ReflectHelper.DefinesAttributeDerivedFrom: internal error: wrong value in the attributes dictionary.");
if (attribute is TAttributeType attributeAsAttributeType)
{
action(attributeAsAttributeType, state);
}
}
}
/// <summary>
/// Gets and caches the attributes for the given type, or method.
/// </summary>
/// <param name="attributeProvider">The member to inspect.</param>
/// <returns>attributes defined.</returns>
internal Attribute[] GetCustomAttributesCached(ICustomAttributeProvider attributeProvider)
{
// If the information is cached, then use it otherwise populate the cache using
// the reflection APIs.
return _attributeCache.GetOrAdd(attributeProvider, GetAttributes);
// We are avoiding func allocation here.
static Attribute[] GetAttributes(ICustomAttributeProvider attributeProvider)
{
// Populate the cache
try
{
object[]? attributes = NotCachedReflectionAccessor.GetCustomAttributesNotCached(attributeProvider);
return attributes is null ? [] : attributes as Attribute[] ?? [.. attributes.Cast<Attribute>()];
}
catch (Exception ex)
{
// Get the exception description
string description;
try
{
// Can throw if the Message or StackTrace properties throw exceptions
description = ex.ToString();
}
catch (Exception ex2)
{
description = string.Format(CultureInfo.CurrentCulture, Resource.ExceptionOccuredWhileGettingTheExceptionDescription, ex.GetType().FullName, ex2.GetType().FullName); // ex.GetType().FullName +
}
PlatformServiceProvider.Instance.AdapterTraceLogger.LogWarning(Resource.FailedToGetCustomAttribute, attributeProvider.GetType().FullName!, description);
return [];
}
}
}
/// <summary>
/// Reflection helper that is accessing Reflection directly, and won't cache the results.
/// </summary>
internal static class NotCachedReflectionAccessor
{
/// <summary>
/// Get custom attributes on a member without cache. Be CAREFUL where you use this, repeatedly accessing reflection without caching the results degrades the performance.
/// </summary>
/// <param name="attributeProvider">Member for which attributes needs to be retrieved.</param>
/// <returns>All attributes of give type on member.</returns>
public static object[]? GetCustomAttributesNotCached(ICustomAttributeProvider attributeProvider)
{
object[] attributesArray = attributeProvider is MemberInfo memberInfo
? PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes(memberInfo)
: PlatformServiceProvider.Instance.ReflectionOperations.GetCustomAttributes((Assembly)attributeProvider, typeof(Attribute));
return attributesArray; // TODO: Investigate if we rely on NRE
}
}
internal /* for tests */ void ClearCache()
// Tests manipulate the platform reflection provider, and we end up caching different attributes than the class / method actually has.
=> _attributeCache.Clear();
}