-
Notifications
You must be signed in to change notification settings - Fork 193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert SourceGenerator_RazorFiles_Works to Microsoft.CodeAnalysis.Testing #8356
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,20 +9,23 @@ | |
using System.Diagnostics.CodeAnalysis; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Testing; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.Extensions.DependencyModel; | ||
using Microsoft.Extensions.DependencyModel.Resolution; | ||
using Xunit; | ||
using Xunit.Sdk; | ||
|
||
namespace Microsoft.NET.Sdk.Razor.SourceGenerators | ||
{ | ||
using Verify = Verifiers.CSharpSourceGeneratorVerifier<RazorSourceGenerator>; | ||
|
||
public class RazorSourceGeneratorTests | ||
{ | ||
private static readonly Project _baseProject = CreateBaseProject(); | ||
|
@@ -31,42 +34,18 @@ public class RazorSourceGeneratorTests | |
public async Task SourceGenerator_RazorFiles_Works() | ||
{ | ||
// Arrange | ||
var project = CreateTestProject(new() | ||
var test = new RazorTest | ||
{ | ||
["Pages/Index.razor"] = "<h1>Hello world</h1>", | ||
}); | ||
|
||
var compilation = await project.GetCompilationAsync(); | ||
var driver = await GetDriverAsync(project); | ||
|
||
var result = RunGenerator(compilation!, ref driver) | ||
.VerifyPageOutput( | ||
@"#pragma checksum ""Pages/Index.razor"" ""{ff1816ec-aa5e-4d10-87f7-6f4963833460}"" ""6b5db227a6aa2228c777b0771108b184b1fc5df3"" | ||
// <auto-generated/> | ||
#pragma warning disable 1591 | ||
namespace MyApp.Pages | ||
{ | ||
#line hidden | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Components; | ||
public partial class Index : global::Microsoft.AspNetCore.Components.ComponentBase | ||
{ | ||
#pragma warning disable 1998 | ||
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) | ||
{ | ||
__builder.AddMarkupContent(0, ""<h1>Hello world</h1>""); | ||
} | ||
#pragma warning restore 1998 | ||
} | ||
} | ||
#pragma warning restore 1591 | ||
"); | ||
TestState = | ||
{ | ||
AdditionalFiles = | ||
{ | ||
("/0/Pages/Index.razor", "<h1>Hello world</h1>") | ||
}, | ||
}, | ||
}; | ||
|
||
Assert.Empty(result.Diagnostics); | ||
Assert.Single(result.GeneratedSources); | ||
await test.AddMetadata().AddGeneratedSources().RunAsync(); | ||
} | ||
|
||
internal class InMemoryAdditionalText : AdditionalText | ||
|
@@ -2512,6 +2491,43 @@ public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string>? as | |
} | ||
} | ||
|
||
private class RazorTest : Verify.Test | ||
{ | ||
public RazorTest([CallerFilePath] string? testFile = null, [CallerMemberName] string? testMethod = null) | ||
: base(testFile, testMethod) | ||
{ | ||
// Don't resolve any reference assemblies from NuGet | ||
ReferenceAssemblies = new ReferenceAssemblies("custom"); | ||
Comment on lines
+2499
to
+2500
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 This line ensures that we don't hit NuGet for anything during testing (unless a specific test overrides this property to a different value). |
||
|
||
foreach (var defaultCompileLibrary in DependencyContext.Load(typeof(RazorSourceGeneratorTests).Assembly)!.CompileLibraries) | ||
{ | ||
foreach (var resolveReferencePath in defaultCompileLibrary.ResolveReferencePaths(new AppLocalResolver())) | ||
{ | ||
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(resolveReferencePath)); | ||
} | ||
} | ||
|
||
// The deps file in the project is incorrect and does not contain "compile" nodes for some references. | ||
// However these binaries are always present in the bin output. As a "temporary" workaround, we'll add | ||
// every dll file that's present in the test's build output as a metadatareference. | ||
foreach (var assembly in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.dll")) | ||
{ | ||
if (!TestState.AdditionalReferences.Any(c => string.Equals(Path.GetFileNameWithoutExtension(c.Display), Path.GetFileNameWithoutExtension(assembly), StringComparison.OrdinalIgnoreCase))) | ||
{ | ||
TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(assembly)); | ||
} | ||
} | ||
Comment on lines
+2502
to
+2519
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 This manually adds |
||
} | ||
|
||
public NullableContextOptions NullableContextOptions { get; set; } = NullableContextOptions.Enable; | ||
|
||
protected override CodeAnalysis.CompilationOptions CreateCompilationOptions() | ||
{ | ||
var options = (CSharpCompilationOptions)base.CreateCompilationOptions(); | ||
return options.WithNullableContextOptions(NullableContextOptions); | ||
} | ||
} | ||
|
||
private static Project CreateBaseProject() | ||
{ | ||
var projectId = ProjectId.CreateNewId(debugName: "TestProject"); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
#pragma checksum "/0/Pages/Index.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6b5db227a6aa2228c777b0771108b184b1fc5df3" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 This line is the only change to this file relative to the original string. The change presumably occurred because AdditionalFiles now includes an absolute path to the razor source file. It's not clear to me whether a full build with the source generator running will use an absolute path here or a relative path here. If it's a relative path, I'm not sure where it's derived from. |
||
// <auto-generated/> | ||
#pragma warning disable 1591 | ||
namespace MyApp.Pages | ||
{ | ||
#line hidden | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Components; | ||
public partial class Index : global::Microsoft.AspNetCore.Components.ComponentBase | ||
{ | ||
#pragma warning disable 1998 | ||
protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) | ||
{ | ||
__builder.AddMarkupContent(0, "<h1>Hello world</h1>"); | ||
} | ||
#pragma warning restore 1998 | ||
} | ||
} | ||
#pragma warning restore 1591 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
#nullable enable | ||
|
||
// Uncomment the following line to write expected files to disk | ||
////#define WRITE_EXPECTED | ||
|
||
#if WRITE_EXPECTED | ||
#warning WRITE_EXPECTED is fine for local builds, but should not be merged to the main branch. | ||
#endif | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Globalization; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Testing; | ||
using Microsoft.CodeAnalysis.Testing; | ||
using Microsoft.CodeAnalysis.Testing.Verifiers; | ||
|
||
namespace Microsoft.NET.Sdk.Razor.SourceGenerators.Verifiers | ||
{ | ||
public static partial class CSharpSourceGeneratorVerifier<TSourceGenerator> | ||
where TSourceGenerator : IIncrementalGenerator, new() | ||
{ | ||
public class Test : CSharpSourceGeneratorTest<EmptySourceGeneratorProvider, XUnitVerifier> | ||
{ | ||
private readonly string? _testFile; | ||
private readonly string? _testMethod; | ||
|
||
public Test([CallerFilePath] string? testFile = null, [CallerMemberName] string? testMethod = null) | ||
{ | ||
CompilerDiagnostics = CompilerDiagnostics.Warnings; | ||
|
||
_testFile = testFile; | ||
_testMethod = testMethod; | ||
|
||
#if WRITE_EXPECTED | ||
TestBehaviors |= TestBehaviors.SkipGeneratedSourcesCheck; | ||
#endif | ||
} | ||
|
||
public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; | ||
|
||
protected override IEnumerable<Type> GetSourceGenerators() | ||
{ | ||
yield return typeof(TSourceGenerator); | ||
} | ||
|
||
protected override CompilationOptions CreateCompilationOptions() | ||
{ | ||
var compilationOptions = (CSharpCompilationOptions)base.CreateCompilationOptions(); | ||
return compilationOptions | ||
.WithAllowUnsafe(false) | ||
.WithWarningLevel(99) | ||
.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItem("CS8019", ReportDiagnostic.Suppress)); | ||
} | ||
|
||
protected override ParseOptions CreateParseOptions() | ||
{ | ||
return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); | ||
} | ||
|
||
protected override async Task<(Compilation compilation, ImmutableArray<Diagnostic> generatorDiagnostics)> GetProjectCompilationAsync(Project project, IVerifier verifier, CancellationToken cancellationToken) | ||
{ | ||
var resourceDirectory = Path.Combine(Path.GetDirectoryName(_testFile)!, "Resources", _testMethod!); | ||
|
||
var (compilation, generatorDiagnostics) = await base.GetProjectCompilationAsync(project, verifier, cancellationToken); | ||
var expectedNames = new HashSet<string>(); | ||
foreach (var tree in compilation.SyntaxTrees.Skip(project.DocumentIds.Count)) | ||
{ | ||
WriteTreeToDiskIfNecessary(tree, resourceDirectory); | ||
expectedNames.Add(Path.GetFileName(tree.FilePath)); | ||
} | ||
|
||
var currentTestPrefix = $"{typeof(RazorSourceGeneratorTests).Assembly.GetName().Name}.Resources.{_testMethod}."; | ||
foreach (var name in GetType().Assembly.GetManifestResourceNames()) | ||
{ | ||
if (!name.StartsWith(currentTestPrefix, StringComparison.Ordinal)) | ||
{ | ||
continue; | ||
} | ||
|
||
if (!expectedNames.Contains(name[currentTestPrefix.Length..])) | ||
{ | ||
throw new InvalidOperationException($"Unexpected test resource: {name[currentTestPrefix.Length..]}"); | ||
} | ||
} | ||
|
||
return (compilation, generatorDiagnostics); | ||
} | ||
|
||
public Test AddMetadata() | ||
{ | ||
var globalConfig = new StringBuilder(@"is_global = true | ||
|
||
build_property.RazorConfiguration = Default | ||
build_property.RootNamespace = MyApp | ||
build_property.RazorLangVersion = Latest | ||
build_property.GenerateRazorMetadataSourceChecksumAttributes = false | ||
"); | ||
|
||
foreach (var (filename, _) in TestState.AdditionalFiles) | ||
{ | ||
globalConfig.AppendLine(CultureInfo.InvariantCulture, $@"[{filename}] | ||
build_metadata.AdditionalFiles.TargetPath = {Convert.ToBase64String(Encoding.UTF8.GetBytes(getRelativeFilePath(filename)))}"); | ||
} | ||
|
||
TestState.AnalyzerConfigFiles.Add(("/.globalconfig", globalConfig.ToString())); | ||
|
||
return this; | ||
|
||
static string getRelativeFilePath(string absolutePath) | ||
{ | ||
if (absolutePath.StartsWith("/0/", StringComparison.Ordinal)) | ||
{ | ||
return absolutePath["/0/".Length..]; | ||
} | ||
else if (absolutePath.StartsWith("/", StringComparison.Ordinal)) | ||
{ | ||
return absolutePath["/".Length..]; | ||
} | ||
else | ||
{ | ||
return absolutePath; | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Loads expected generated sources from embedded resources based on the test name. | ||
/// </summary> | ||
/// <param name="testMethod">The current test method name.</param> | ||
/// <returns>The current <see cref="Test"/> instance.</returns> | ||
public Test AddGeneratedSources([CallerMemberName] string? testMethod = null) | ||
{ | ||
var expectedPrefix = $"{typeof(RazorSourceGeneratorTests).Assembly.GetName().Name}.Resources.{testMethod}."; | ||
foreach (var resourceName in typeof(Test).Assembly.GetManifestResourceNames()) | ||
{ | ||
if (!resourceName.StartsWith(expectedPrefix, StringComparison.Ordinal)) | ||
{ | ||
continue; | ||
} | ||
|
||
using var resourceStream = typeof(RazorSourceGeneratorTests).Assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException(); | ||
using var reader = new StreamReader(resourceStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true); | ||
var name = resourceName[expectedPrefix.Length..]; | ||
TestState.GeneratedSources.Add((typeof(RazorSourceGenerator), name, reader.ReadToEnd())); | ||
} | ||
|
||
// An error will be reported if there are no sources or generated sources in the compilation. To bypass | ||
// during the initial test construction, we add a default empty generated source knowing that it will | ||
// not be validated. | ||
if (TestBehaviors.HasFlag(TestBehaviors.SkipGeneratedSourcesCheck) && !TestState.Sources.Any() && !TestState.GeneratedSources.Any()) | ||
{ | ||
TestState.GeneratedSources.Add(("/ignored_file", "")); | ||
} | ||
|
||
return this; | ||
} | ||
|
||
[Conditional("WRITE_EXPECTED")] | ||
private static void WriteTreeToDiskIfNecessary(SyntaxTree tree, string resourceDirectory) | ||
{ | ||
if (tree.Encoding is null) | ||
{ | ||
throw new ArgumentException("Syntax tree encoding was not specified"); | ||
} | ||
|
||
var name = Path.GetFileName(tree.FilePath); | ||
var filePath = Path.Combine(resourceDirectory, name); | ||
Directory.CreateDirectory(resourceDirectory); | ||
File.WriteAllText(filePath, tree.GetText().ToString(), tree.Encoding); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.CodeAnalysis; | ||
|
||
namespace Microsoft.NET.Sdk.Razor.SourceGenerators.Verifiers | ||
{ | ||
public static partial class CSharpSourceGeneratorVerifier<TSourceGenerator> | ||
where TSourceGenerator : IIncrementalGenerator, new() | ||
{ | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 the page output was moved to an embedded resource, which is generated by modifying the
#define
at the top of the test file and otherwise loaded from disk for the test