-
Notifications
You must be signed in to change notification settings - Fork 759
/
MemberDataAttributeBase.cs
147 lines (124 loc) · 6.16 KB
/
MemberDataAttributeBase.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
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Xunit.Sdk;
namespace Xunit
{
/// <summary>
/// Provides a base class for attributes that will provide member data. The member data must return
/// something compatible with <see cref="IEnumerable"/>.
/// Caution: the property is completely enumerated by .ToList() before any test is run. Hence it should return independent object sets.
/// </summary>
[CLSCompliant(false)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class MemberDataAttributeBase : DataAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="MemberDataAttributeBase"/> class.
/// </summary>
/// <param name="memberName">The name of the public static member on the test class that will provide the test data</param>
/// <param name="parameters">The parameters for the member (only supported for methods; ignored for everything else)</param>
protected MemberDataAttributeBase(string memberName, object[] parameters)
{
MemberName = memberName;
Parameters = parameters;
}
/// <summary>
/// Returns <c>true</c> if the data attribute wants to skip enumerating data during discovery.
/// This will cause the theory to yield a single test case for all data, and the data discovery
/// will be during test execution instead of discovery.
/// </summary>
public bool DisableDiscoveryEnumeration { get; set; }
/// <summary>
/// Gets the member name.
/// </summary>
public string MemberName { get; private set; }
/// <summary>
/// Gets or sets the type to retrieve the member from. If not set, then the property will be
/// retrieved from the unit test class.
/// </summary>
public Type MemberType { get; set; }
/// <summary>
/// Gets or sets the parameters passed to the member. Only supported for static methods.
/// </summary>
public object[] Parameters { get; private set; }
/// <inheritdoc/>
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
Guard.ArgumentNotNull("testMethod", testMethod);
var type = MemberType ?? testMethod.DeclaringType;
var accessor = GetPropertyAccessor(type) ?? GetFieldAccessor(type) ?? GetMethodAccessor(type);
if (accessor == null)
{
var parameterText = Parameters?.Length > 0 ? $" with parameter types: {string.Join(", ", Parameters.Select(p => p?.GetType().FullName ?? "(null)"))}" : "";
throw new ArgumentException($"Could not find public static member (property, field, or method) named '{MemberName}' on {type.FullName}{parameterText}");
}
var obj = accessor();
if (obj == null)
return null;
var dataItems = obj as IEnumerable;
if (dataItems == null)
throw new ArgumentException($"Property {MemberName} on {type.FullName} did not return IEnumerable");
return dataItems.Cast<object>().Select(item => ConvertDataItem(testMethod, item));
}
/// <summary>
/// Converts an item yielded by the data member to an object array, for return from <see cref="GetData"/>.
/// </summary>
/// <param name="testMethod">The method that is being tested.</param>
/// <param name="item">An item yielded from the data member.</param>
/// <returns>An <see cref="T:object[]"/> suitable for return from <see cref="GetData"/>.</returns>
protected abstract object[] ConvertDataItem(MethodInfo testMethod, object item);
Func<object> GetFieldAccessor(Type type)
{
FieldInfo fieldInfo = null;
for (var reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
{
fieldInfo = reflectionType.GetRuntimeField(MemberName);
if (fieldInfo != null)
break;
}
if (fieldInfo == null || !fieldInfo.IsStatic)
return null;
return () => fieldInfo.GetValue(null);
}
Func<object> GetMethodAccessor(Type type)
{
MethodInfo methodInfo = null;
var parameterTypes = Parameters == null ? new Type[0] : Parameters.Select(p => p?.GetType()).ToArray();
for (var reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
{
methodInfo = reflectionType.GetRuntimeMethods()
.FirstOrDefault(m => m.Name == MemberName && ParameterTypesCompatible(m.GetParameters(), parameterTypes));
if (methodInfo != null)
break;
}
if (methodInfo == null || !methodInfo.IsStatic)
return null;
return () => methodInfo.Invoke(null, Parameters);
}
Func<object> GetPropertyAccessor(Type type)
{
PropertyInfo propInfo = null;
for (var reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
{
propInfo = reflectionType.GetRuntimeProperty(MemberName);
if (propInfo != null)
break;
}
if (propInfo == null || propInfo.GetMethod == null || !propInfo.GetMethod.IsStatic)
return null;
return () => propInfo.GetValue(null, null);
}
static bool ParameterTypesCompatible(ParameterInfo[] parameters, Type[] parameterTypes)
{
if (parameters?.Length != parameterTypes.Length)
return false;
for (int idx = 0; idx < parameters.Length; ++idx)
if (parameterTypes[idx] != null && !parameters[idx].ParameterType.GetTypeInfo().IsAssignableFrom(parameterTypes[idx].GetTypeInfo()))
return false;
return true;
}
}
}