-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathTestHelper.cs
More file actions
240 lines (202 loc) · 9.88 KB
/
TestHelper.cs
File metadata and controls
240 lines (202 loc) · 9.88 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
// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved.
// Licensed to the ReactiveUI and contributors under one or more agreements.
// The ReactiveUI and contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
using FluentAssertions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Versioning;
using ReactiveMarbles.NuGet.Helpers;
using ReactiveMarbles.SourceGenerator.TestNuGetHelper.Compilation;
using ReactiveUI.SourceGenerators;
using ReactiveUI.SourceGenerators.WinForms;
using Xunit.Abstractions;
namespace ReactiveUI.SourceGenerator.Tests;
/// <summary>
/// A helper class to facilitate the testing of incremental source generators.
/// It provides utilities to initialize dependencies, run generators, and verify the output.
/// </summary>
/// <typeparam name="T">Type of Incremental Generator.</typeparam>
/// <seealso cref="System.IDisposable" />
/// <param name="testOutput">The test output helper for capturing test logs.</param>
public sealed class TestHelper<T>(ITestOutputHelper testOutput) : IDisposable
where T : IIncrementalGenerator, new()
{
#pragma warning disable CS0618 // Type or member is obsolete
/// <summary>
/// Represents the NuGet library dependency for the Splat library.
/// </summary>
private static readonly LibraryRange SplatLibrary =
new("Splat", VersionRange.AllStableFloating, LibraryDependencyTarget.Package);
/// <summary>
/// Represents the NuGet library dependency for the ReactiveUI library.
/// </summary>
private static readonly LibraryRange ReactiveuiLibrary =
new("ReactiveUI", VersionRange.AllStableFloating, LibraryDependencyTarget.Package);
#pragma warning restore CS0618 // Type or member is obsolete
private static readonly string mscorlibPath = Path.Combine(
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(),
"mscorlib.dll");
private static readonly MetadataReference[] References =
[
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(typeof(T).Assembly.Location),
MetadataReference.CreateFromFile(typeof(TestHelper<T>).Assembly.Location),
// Create mscorlib Reference
MetadataReference.CreateFromFile(mscorlibPath)
// Wpf references
////MetadataReference.CreateFromFile(Assembly.Load("PresentationCore").Location),
////MetadataReference.CreateFromFile(Assembly.Load("PresentationFramework").Location),
////MetadataReference.CreateFromFile(Assembly.Load("WindowsBase").Location),
////MetadataReference.CreateFromFile(Assembly.Load("System.Xaml").Location),
];
/// <summary>
/// Holds the compiler instance used for event-related code generation.
/// </summary>
private EventBuilderCompiler? _eventCompiler;
/// <summary>
/// Verifieds the file path.
/// </summary>
/// <returns>
/// A string.
/// </returns>
public string VerifiedFilePath()
{
var name = typeof(T).Name;
return name switch
{
nameof(ReactiveGenerator) => "REACTIVE",
nameof(ReactiveCommandGenerator) => "REACTIVECMD",
nameof(ObservableAsPropertyGenerator) => "OAPH",
nameof(IViewForGenerator) => "IVIEWFOR",
nameof(RoutedControlHostGenerator) => "ROUTEDHOST",
nameof(ViewModelControlHostGenerator) => "CONTROLHOST",
nameof(BindableDerivedListGenerator) => "DERIVEDLIST",
_ => name,
};
}
/// <summary>
/// Asynchronously initializes the source generator helper by downloading required packages.
/// </summary>
/// <returns>A task representing the asynchronous initialization operation.</returns>
public async Task InitializeAsync()
{
NuGetFramework[] targetFrameworks = [new NuGetFramework(".NETCoreApp", new Version(8, 0, 0, 0))];
// Download necessary NuGet package files.
var inputGroup = await NuGetPackageHelper.DownloadPackageFilesAndFolder(
[SplatLibrary, ReactiveuiLibrary],
targetFrameworks,
packageOutputDirectory: null).ConfigureAwait(false);
// Initialize the event compiler with downloaded packages and target framework.
var framework = targetFrameworks[0];
_eventCompiler = new EventBuilderCompiler(inputGroup, inputGroup, framework);
}
/// <summary>
/// Tests a generator expecting it to fail by throwing an <see cref="InvalidOperationException"/>.
/// </summary>
/// <param name="source">The source code to test.</param>
public void TestFail(
string source)
{
if (_eventCompiler is null)
{
throw new InvalidOperationException("Must have valid compiler instance.");
}
var utility = new SourceGeneratorUtility(x => testOutput.WriteLine(x));
#pragma warning disable IDE0053 // Use expression body for lambda expression
#pragma warning disable RCS1021 // Convert lambda expression body to expression body
Assert.Throws<InvalidOperationException>(() => { RunGeneratorAndCheck(source); });
#pragma warning restore RCS1021 // Convert lambda expression body to expression body
#pragma warning restore IDE0053 // Use expression body for lambda expression
}
/// <summary>
/// Tests a generator expecting it to pass successfully.
/// </summary>
/// <param name="source">The source code to test.</param>
/// <param name="withPreDiagnosics">if set to <c>true</c> [with pre diagnosics].</param>
/// <returns>
/// The driver.
/// </returns>
/// <exception cref="InvalidOperationException">Must have valid compiler instance.</exception>
/// <exception cref="ArgumentNullException">callerType.</exception>
public SettingsTask TestPass(
string source,
bool withPreDiagnosics = false)
{
if (_eventCompiler is null)
{
throw new InvalidOperationException("Must have valid compiler instance.");
}
return RunGeneratorAndCheck(source, withPreDiagnosics);
}
/// <inheritdoc/>
public void Dispose() => _eventCompiler?.Dispose();
/// <summary>
/// Runs the specified source generator and validates the generated code.
/// </summary>
/// <param name="code">The code to be parsed and processed by the generator.</param>
/// <param name="withPreDiagnosics">if set to <c>true</c> [with pre diagnosics].</param>
/// <param name="rerunCompilation">Indicates whether to rerun the compilation after running the generator.</param>
/// <returns>
/// The generator driver used to run the generator.
/// </returns>
/// <exception cref="InvalidOperationException">Thrown if the compiler instance is not valid or if the compilation fails.</exception>
public SettingsTask RunGeneratorAndCheck(
string code,
bool withPreDiagnosics = false,
bool rerunCompilation = true)
{
if (_eventCompiler is null)
{
throw new InvalidOperationException("Must have a valid compiler instance.");
}
// Collect required assembly references.
var assemblies = new HashSet<MetadataReference>(
Basic.Reference.Assemblies.Net80.References.All
.Concat(References)
.Concat(_eventCompiler.Modules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName)))
.Concat(_eventCompiler.ReferencedModules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName)))
.Concat(_eventCompiler.NeededModules.Select(x => MetadataReference.CreateFromFile(x.PEFile!.FileName))));
var syntaxTree = CSharpSyntaxTree.ParseText(code, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp13));
// Create a compilation with the provided source code.
var compilation = CSharpCompilation.Create(
"TestProject",
[syntaxTree],
assemblies,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true));
if (withPreDiagnosics)
{
// Validate diagnostics before running the generator.
var prediagnostics = compilation.GetDiagnostics()
.Where(d => d.Severity > DiagnosticSeverity.Warning)
.ToList();
prediagnostics.Should().BeEmpty();
}
var generator = new T();
var driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
if (rerunCompilation)
{
// Run the generator and capture diagnostics.
var rerunDriver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var diagnostics);
// If any warnings or errors are found, log them to the test output before throwing an exception.
var offendingDiagnostics = diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Warning)
.ToList();
if (offendingDiagnostics.Count > 0)
{
foreach (var diagnostic in offendingDiagnostics)
{
testOutput.WriteLine($"Diagnostic: {diagnostic.Id} - {diagnostic.GetMessage()}");
}
throw new InvalidOperationException("Compilation failed due to the above diagnostics.");
}
return VerifyGenerator(rerunDriver);
}
// If rerun is not needed, simply run the generator.
return VerifyGenerator(driver.RunGenerators(compilation));
}
private SettingsTask VerifyGenerator(GeneratorDriver driver) => Verify(driver).UseDirectory(VerifiedFilePath()).ScrubLinesContaining("[global::System.CodeDom.Compiler.GeneratedCode(\"");
}