Skip to content

Commit 9e17ce8

Browse files
authored
Added [LabsUITestMethod] attribute and source generator (#156)
* Created UIControlTestMethod generator + unit tests * Added new source generators to template * Migrated existing tests to new source generator attribute * Refactored to pull control instance directly from method params. Improved control lifetime. * Cleanup outdated comments * Added mandatory content cleanup for UI tests * Fixed diagnostic ID to match project name initials * Fixed source generator to only check public constructors for LUITM0001 * Fixed test setup/cleanup invoking on the wrong thread * Added new source generator to individual project solutions * Reverted to toolkit extension for dispatcher EnqueueAsync * Remove whitespace * Fixed non file-scoped namespaces * Added missing file header * Removed unused using directives
1 parent 107cc4b commit 9e17ce8

File tree

18 files changed

+1079
-266
lines changed

18 files changed

+1079
-266
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
13+
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
14+
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod\CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod.csproj" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
6+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod.Diagnostics;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
11+
namespace CommunityToolkit.Labs.Core.SourceGenerators.Tests;
12+
13+
[TestClass]
14+
public partial class LabsUITestMethodTests
15+
{
16+
private const string DispatcherQueueDefinition = @"
17+
namespace MyApp
18+
{
19+
public partial class Test
20+
{
21+
public System.Threading.Tasks.Task EnqueueAsync<T>(System.Func<System.Threading.Tasks.Task<T>> function) => System.Threading.Tasks.Task.Run(function);
22+
23+
public System.Threading.Tasks.Task EnqueueAsync(System.Action function) => System.Threading.Tasks.Task.Run(function);
24+
}
25+
}
26+
";
27+
28+
[TestMethod]
29+
public void TestControlHasConstructorWithParameters()
30+
{
31+
string source = @"
32+
using System.ComponentModel;
33+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
34+
35+
namespace MyApp
36+
{
37+
public partial class Test
38+
{
39+
public System.Threading.Tasks.Task LoadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
40+
public System.Threading.Tasks.Task UnloadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
41+
42+
[LabsUITestMethod]
43+
public void TestMethod(MyControl control)
44+
{
45+
}
46+
}
47+
48+
public class MyControl : Microsoft.UI.Xaml.FrameworkElement
49+
{
50+
public MyControl(string id)
51+
{
52+
}
53+
}
54+
}
55+
56+
namespace Microsoft.UI.Xaml
57+
{
58+
public class FrameworkElement { }
59+
}";
60+
61+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition, DiagnosticDescriptors.TestControlHasConstructorWithParameters.Id);
62+
}
63+
64+
[TestMethod]
65+
public void Async_Mux_NoErrors()
66+
{
67+
string source = @"
68+
using System.ComponentModel;
69+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
70+
71+
namespace MyApp
72+
{
73+
public partial class Test
74+
{
75+
public System.Threading.Tasks.Task LoadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
76+
public System.Threading.Tasks.Task UnloadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
77+
78+
[LabsUITestMethod]
79+
public async System.Threading.Tasks.Task TestMethod(MyControl control)
80+
{
81+
}
82+
}
83+
84+
public class MyControl : Microsoft.UI.Xaml.FrameworkElement
85+
{
86+
}
87+
}
88+
89+
namespace Microsoft.UI.Xaml
90+
{
91+
public class FrameworkElement { }
92+
}";
93+
94+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
95+
}
96+
97+
[TestMethod]
98+
public void Async_Wux_NoErrors()
99+
{
100+
string source = @"
101+
using System.ComponentModel;
102+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
103+
104+
namespace MyApp
105+
{
106+
public partial class Test
107+
{
108+
public System.Threading.Tasks.Task LoadTestContentAsync(Windows.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
109+
public System.Threading.Tasks.Task UnloadTestContentAsync(Windows.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
110+
111+
[LabsUITestMethod]
112+
public async System.Threading.Tasks.Task TestMethod(MyControl control)
113+
{
114+
}
115+
}
116+
117+
public class MyControl : Windows.UI.Xaml.FrameworkElement
118+
{
119+
}
120+
}
121+
122+
namespace Windows.UI.Xaml
123+
{
124+
public class FrameworkElement { }
125+
}";
126+
127+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
128+
}
129+
130+
[TestMethod]
131+
public void Async_NoMethodParams_NoErrors()
132+
{
133+
string source = @"
134+
using System.ComponentModel;
135+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
136+
137+
namespace MyApp
138+
{
139+
public partial class Test
140+
{
141+
[LabsUITestMethod]
142+
public async System.Threading.Tasks.Task TestMethod()
143+
{
144+
}
145+
}
146+
}";
147+
148+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
149+
}
150+
151+
[TestMethod]
152+
public void Synchronous_Mux_NoErrors()
153+
{
154+
string source = @"
155+
using System.ComponentModel;
156+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
157+
158+
namespace MyApp
159+
{
160+
public partial class Test
161+
{
162+
public System.Threading.Tasks.Task LoadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
163+
public System.Threading.Tasks.Task UnloadTestContentAsync(Microsoft.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
164+
165+
[LabsUITestMethod]
166+
public void TestMethod(MyControl control)
167+
{
168+
}
169+
}
170+
171+
public class MyControl : Microsoft.UI.Xaml.FrameworkElement
172+
{
173+
}
174+
}
175+
176+
namespace Microsoft.UI.Xaml
177+
{
178+
public class FrameworkElement { }
179+
}";
180+
181+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
182+
}
183+
184+
[TestMethod]
185+
public void Synchronous_Wux_NoErrors()
186+
{
187+
string source = @"
188+
using System.ComponentModel;
189+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
190+
191+
namespace MyApp
192+
{
193+
public partial class Test
194+
{
195+
public System.Threading.Tasks.Task LoadTestContentAsync(Windows.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
196+
public System.Threading.Tasks.Task UnloadTestContentAsync(Windows.UI.Xaml.FrameworkElement content) => System.Threading.Tasks.Task.CompletedTask;
197+
198+
[LabsUITestMethod]
199+
public void TestMethod(MyControl control)
200+
{
201+
}
202+
}
203+
204+
public class MyControl : Windows.UI.Xaml.FrameworkElement
205+
{
206+
}
207+
}
208+
209+
namespace Windows.UI.Xaml
210+
{
211+
public class FrameworkElement { }
212+
}";
213+
214+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
215+
}
216+
217+
[TestMethod]
218+
public void Synchronous_NoMethodParams_NoErrors()
219+
{
220+
string source = @"
221+
using System.ComponentModel;
222+
using CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod;
223+
224+
namespace MyApp
225+
{
226+
public partial class Test
227+
{
228+
[LabsUITestMethod]
229+
public void TestMethod()
230+
{
231+
}
232+
}
233+
}";
234+
235+
VerifyGeneratedDiagnostics<LabsUITestMethodGenerator>(source + DispatcherQueueDefinition);
236+
}
237+
238+
/// <summary>
239+
/// Verifies the output of a source generator.
240+
/// </summary>
241+
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
242+
/// <param name="source">The input source to process.</param>
243+
/// <param name="markdown">The input documentation info to process.</param>
244+
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
245+
private static void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
246+
where TGenerator : class, IIncrementalGenerator, new()
247+
{
248+
VerifyGeneratedDiagnostics<TGenerator>(CSharpSyntaxTree.ParseText(source), diagnosticsIds);
249+
}
250+
251+
/// <summary>
252+
/// Verifies the output of a source generator.
253+
/// </summary>
254+
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
255+
/// <param name="syntaxTree">The input source tree to process.</param>
256+
/// <param name="markdown">The input documentation info to process.</param>
257+
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
258+
private static void VerifyGeneratedDiagnostics<TGenerator>(SyntaxTree syntaxTree, params string[] diagnosticsIds)
259+
where TGenerator : class, IIncrementalGenerator, new()
260+
{
261+
var attributeType = typeof(LabsUITestMethodAttribute);
262+
263+
var references =
264+
from assembly in AppDomain.CurrentDomain.GetAssemblies()
265+
where !assembly.IsDynamic
266+
let reference = MetadataReference.CreateFromFile(assembly.Location)
267+
select reference;
268+
269+
var compilation = CSharpCompilation.Create(
270+
"original.Sample",
271+
new[] { syntaxTree },
272+
references,
273+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
274+
275+
var compilationDiagnostics = compilation.GetDiagnostics();
276+
277+
Assert.IsTrue(compilationDiagnostics.All(x => x.Severity != DiagnosticSeverity.Error), $"Expected no compilation errors before source generation. Got: \n{string.Join("\n", compilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => $"[{x.Id}: {x.GetMessage()}]"))}");
278+
279+
IIncrementalGenerator generator = new TGenerator();
280+
281+
GeneratorDriver driver =
282+
CSharpGeneratorDriver
283+
.Create(generator)
284+
.WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
285+
286+
_ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> diagnostics);
287+
288+
HashSet<string> resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet();
289+
var generatedCompilationDiaghostics = outputCompilation.GetDiagnostics();
290+
291+
Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join(", ", resultingIds)}]");
292+
Assert.IsTrue(generatedCompilationDiaghostics.All(x => x.Severity != DiagnosticSeverity.Error), $"Expected no generated compilation errors. Got: \n{string.Join("\n", generatedCompilationDiaghostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => $"[{x.Id}: {x.GetMessage()}]"))}");
293+
294+
GC.KeepAlive(attributeType);
295+
}
296+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<WarningsAsErrors>nullable</WarningsAsErrors>
7+
<LangVersion>10.0</LangVersion>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
12+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
13+
</ItemGroup>
14+
</Project>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Labs.Core.SourceGenerators.LabsUITestMethod.Diagnostics;
8+
9+
/// <summary>
10+
/// A container for all <see cref="DiagnosticDescriptor"/> instances for errors reported by analyzers in this project.
11+
/// </summary>
12+
public static class DiagnosticDescriptors
13+
{
14+
/// <summary>
15+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating that a test method decorated with <see cref="LabsUITestMethodAttribute"/> asks for a control instance with a non-parameterless constructor.
16+
/// <para>
17+
/// Format: <c>"Cannot generate test with type {0} as it has a constructor with parameters."</c>.
18+
/// </para>
19+
/// </summary>
20+
public static readonly DiagnosticDescriptor TestControlHasConstructorWithParameters = new(
21+
id: "LUITM0001",
22+
title: $"Provided control must not have a constructor with parameters.",
23+
messageFormat: $"Cannot generate test with control {{0}} as it has a constructor with parameters.",
24+
category: typeof(LabsUITestMethodGenerator).FullName,
25+
defaultSeverity: DiagnosticSeverity.Error,
26+
isEnabledByDefault: true,
27+
description: $"Cannot generate test method with provided control.");
28+
}

0 commit comments

Comments
 (0)