forked from msgpack/msgpack-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRoslynAnalyzerUnitTestExplorer.ttinclude
More file actions
339 lines (316 loc) · 12.6 KB
/
RoslynAnalyzerUnitTestExplorer.ttinclude
File metadata and controls
339 lines (316 loc) · 12.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
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Threading.Tasks" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\System.Collections.Immutable.dll" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ import namespace="Microsoft.CodeAnalysis.MSBuild" #>
<#@ import namespace="EnvDTE" #>
<#+
// This file is borrowed from UniRX (https://github.com/neuecc/UniRx/blob/master/Assets/UnitTests/UnitTests.tt)
// This file implicitly depends on DirectUnitTestDriverCore.ttinclude.
/// <summary>
/// Explores test classes/methods using EnvDTE and Roslyn Analyzer.
/// ><summary>
private class TestClassExplorer
{
/// <summary>
/// The type full name of "test class" inidicator attribute type.
/// ><summary>
private readonly string _testClassAttributeFullName;
/// <summary>
/// The type full name of "test method" inidicator attribute type.
/// ><summary>
private readonly string _testMethodAttributeFullName;
/// <summary>
/// The type full name of "fixture level test setip routine" inidicator attribute type.
/// ><summary>
private readonly string _fixtureSetupAttributeFullName;
/// <summary>
/// The type full name of "fixture level test cleanup routine" inidicator attribute type.
/// ><summary>
private readonly string _fixtureCleanupAttributeFullName;
/// <summary>
/// The type full name of "per test setip routine" inidicator attribute type.
/// ><summary>
private readonly string _testSetupAttributeFullName;
/// <summary>
/// The type full name of "per test cleanup routine" inidicator attribute type.
/// ><summary>
private readonly string _testCleanupAttributeFullName;
/// <summary>
/// The type full name of "skipping specified test" inidicator attribute type.
/// ><summary>
private readonly string _testSkippingAttributeFullName;
/// <summary>
/// The type full name of "test case" inidicator attribute type.
/// ><summary>
private readonly string _testCaseAttributeFullName;
/// <summary>
/// The array of type full names of attributes which should mark significant methods including test method, setup method, etc.
/// ><summary>
private readonly string[] _significantMethodAttributeFullNames;
/// <summary>
/// Initializes a new instance.
/// </summary>
/// <param name="testClassAttributeFullName">The type full name of "test class" inidicator attribute type.</param>
/// <param name="testMethodAttributeFullName">The type full name of "test method" inidicator attribute type.</param>
/// <param name="fixtureSetupAttributeFullName">The type full name of "fixture level test setip routine" inidicator attribute type.</param>
/// <param name="fixtureCleanupAttributeFullName">The type full name of "fixture level test cleanup routine" inidicator attribute type.</param>
/// <param name="testSetupAttributeFullName">The type full name of "per test setip routine" inidicator attribute type.</param>
/// <param name="testCleanupAttributeFullName">The type full name of "per test cleanup routine" inidicator attribute type.</param>
/// <param name="testSkippingAttributeFullName">The type full name of "skipping specified test" inidicator attribute type.</param>
/// <param name="testCaseAttributeFullName">The type full name of "test case" inidicator attribute type.</param>
public TestClassExplorer( string testClassAttributeFullName, string testMethodAttributeFullName,
string fixtureSetupAttributeFullName, string fixtureCleanupAttributeFullName,
string testSetupAttributeFullName, string testCleanupAttributeFullName,
string testSkippingAttributeFullName, string testCaseAttributeFullName
)
{
this._testClassAttributeFullName = testClassAttributeFullName;
this._testMethodAttributeFullName = testMethodAttributeFullName;
this._fixtureSetupAttributeFullName = fixtureSetupAttributeFullName;
this._fixtureCleanupAttributeFullName = fixtureCleanupAttributeFullName;
this._testSetupAttributeFullName = testSetupAttributeFullName;
this._testCleanupAttributeFullName = testCleanupAttributeFullName;
this._testSkippingAttributeFullName = testSkippingAttributeFullName;
this._testCaseAttributeFullName = testCaseAttributeFullName;
this._significantMethodAttributeFullNames =
new [] { testMethodAttributeFullName, fixtureSetupAttributeFullName, fixtureCleanupAttributeFullName, testSetupAttributeFullName, testCleanupAttributeFullName };
}
/// <summary>
/// Returns a new instance for NUnit or NUnitLite.
/// </summary>
/// <returns>
/// A new instance for NUnit or NUnitLite.
/// </returns>
public static TestClassExplorer ForNUnit()
{
return
new TestClassExplorer(
"NUnit.Framework.TestFixtureAttribute",
"NUnit.Framework.TestAttribute",
"NUnit.Framework.TestFixtureSetUpAttribute",
"NUnit.Framework.TestFixtureTearDownAttribute",
"NUnit.Framework.SetUpAttribute",
"NUnit.Framework.TearDownAttribute",
"NUnit.Framework.IgnoreAttribute",
"NUnit.Framework.TestCaseAttribute"
);
}
/// <summary>
/// Explores specified projects and returns found test classes.
/// </summary>
/// <param name="host">The host object which implements <see cref="IServiceProvider"/>.</param>
/// <param name="projectName">The name of exploring test project.</param>
/// <returns>
/// A sequence of the found test classes.
/// </returns>
public IEnumerable<TestClass> FindTestClasses( object host, string projectName )
{
var serviceProvider = host as IServiceProvider;
var dte = serviceProvider.GetService( typeof( DTE ) ) as DTE;
var targetProj = dte.Solution.Projects.Cast<EnvDTE.Project>().First( x => x.Name == projectName );
return GetClassesAsync( targetProj.FullName ).Result.OrderBy( x => x.TypeFullName );
}
/// <summary>
/// Explores specified projects and returns found test classes asynchronously.
/// </summary>
/// <param name="csProjectPath">The path to C# project.</param>
/// <returns>
/// A pending asynchronous operation. Its value is sequence of the found test classes.
/// </returns>
private async Task<IEnumerable<TestClass>> GetClassesAsync( string csProjectPath )
{
var workspace = MSBuildWorkspace.Create();
var project = await workspace.OpenProjectAsync( csProjectPath );
var compilation = await project.GetCompilationAsync();
return
compilation.SyntaxTrees
.SelectMany( st =>
{
var semModel = compilation.GetSemanticModel( st );
return
st.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Select( x => semModel.GetDeclaredSymbol( x ) );
}
).SelectMany( x => x.GetMembers(), ( @class, member ) => new { @class, member } )
.Where( x => x.member.GetAttributes().Any( a => this._significantMethodAttributeFullNames.Contains( GetAttributeName( a ) ) ) )
.Distinct()
.GroupBy( x => x.@class.Name )
.Where( g => g.SelectMany( x => x.@class.GetAttributes() ).Any( a => GetAttributeName( a ) == this._testClassAttributeFullName ) )
.Select( g =>
new TestClass(
g.Key,
g.Where( x =>
x.member.GetAttributes().Any( a => GetAttributeName( a ) == this._testMethodAttributeFullName ) // Only test methods
&& x.member.GetAttributes().All( a => GetAttributeName( a ) != this._testSkippingAttributeFullName ) // Excludes "Ignored" method
).Select( x =>
new TestMethod(
x.member.Name,
// only test data from attribute constructor arguments are supported.
x.member.GetAttributes().Where( a => GetAttributeName( a ) == this._testCaseAttributeFullName ).Select( a => GetTestData( a ) ).ToArray()
)
).OrderBy( x => x.Name )
)
{
FixtureSetup = GetSpecialMethodName( g.Select( x => x.member ), this._fixtureSetupAttributeFullName ),
FixtureCleanup = GetSpecialMethodName( g.Select( x => x.member ), this._fixtureCleanupAttributeFullName ),
TestSetup = GetSpecialMethodName( g.Select( x => x.member ), this._testSetupAttributeFullName ),
TestCleanup = GetSpecialMethodName( g.Select( x => x.member ), this._testCleanupAttributeFullName )
}
).OrderBy( x => x.TypeFullName )
.ToArray();
} // GetClassNamesAsync
// From Roslyn source
private static readonly SymbolDisplayFormat QualifiedNameOnlyFormat =
new SymbolDisplayFormat(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces
);
/// <summary>
/// Gets the type full name of the attribute.
/// </summary>
/// <param name="attribute">The attribute.</param>
/// <returns>
/// The type full name of the attribute.
/// </returns>
private static string GetAttributeName( AttributeData attribute )
{
return attribute.AttributeClass.ToDisplayString( QualifiedNameOnlyFormat );
}
/// <summary>
/// Gets test data for specified "test case".
/// </summary>
/// <param name="attribute">The attribute.</param>
/// <returns>
/// The test data for the case. The element should be primitive value types, string or null.
/// </returns>
private static string[] GetTestData( AttributeData attribute )
{
TypedConstant[] constructorArguments = attribute.ConstructorArguments.ToArray();
switch ( constructorArguments.Length )
{
case 1:
{
if ( constructorArguments[ 0 ].Kind == TypedConstantKind.Array )
{
// [Attr(new object[]{ a, b, c })]
// Returns array's content (Values is IEnumerable<TypedConstant>)
return constructorArguments[ 0 ].Values.Select( ToCSharpLiteral ).ToArray();
}
else
{
goto default;
}
}
default:
{
// [Attr( a, b, c )]
return constructorArguments.Select( ToCSharpLiteral ).ToArray();
}
}
}
/// <summary>
/// Converts a Roslyn <see cref="TypedConstant" /> to a C# literal representation.
/// </summary>
/// <param name="constant">A Roslyn <see cref="TypedConstant" />.</param>
/// <returns>A C# literal representation.</returns>
private static string ToCSharpLiteral( TypedConstant constant )
{
if ( constant.IsNull )
{
return "null";
}
switch( constant.Kind )
{
case TypedConstantKind.Enum:
{
var value = constant.Value.ToString();
if ( Regex.IsMatch( value, @"^\d+$" ) )
{
return "( " + constant.Type.Name + " )" + constant.Value;
}
else
{
return constant.Type.Name + "." + constant.Value;
}
}
case TypedConstantKind.Type:
{
// TODO: generic type
return "typeof( " + constant.Value + " )";
}
case TypedConstantKind.Primitive:
{
switch( Type.GetTypeCode( constant.Value.GetType() ) )
{
case TypeCode.String:
{
return "@\"" + constant.Value.ToString().Replace( "\"", "\"\"" ) + "\"";
}
case TypeCode.Char:
{
return "'" + constant.Value + "'";
}
case TypeCode.Int64:
{
return constant.Value + "L";
}
case TypeCode.UInt32:
{
return constant.Value + "U";
}
case TypeCode.UInt64:
{
return constant.Value + "UL";
}
case TypeCode.Single:
{
return constant.Value + "F";
}
case TypeCode.Decimal:
{
return constant.Value + "M";
}
default:
{
return constant.Value.ToString();
}
}
}
default:
{
return "__ERROR(" + constant.Kind + ")__";
}
}
}
/// <summary>
/// Gets the name of the special method which marked with specified attribute.
/// </summary>
/// <param name="m">The sequence of <see cref="ISymbol" /> which represents member of the type.</param>
/// <param name="attributeFullName">The type full name of the attribute.</param>
/// <returns>
/// The name of the special method which marked with specified attribute.
/// </returns>
private static string GetSpecialMethodName( IEnumerable<ISymbol> m, string attributeFullName )
{
var found = m.FirstOrDefault( x => x.GetAttributes().Any( a => GetAttributeName( a ) == attributeFullName ) );
return found == null ? null : found.Name;
}
} // TestClassExplorer
#>