diff --git a/eng/Versions.props b/eng/Versions.props index d3d81f6cb7a..48c8a2f3b4a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -74,7 +74,7 @@ 17.5.101-preview-0002 - 1.1.2-beta1.22512.1 + 1.1.2-beta1.23115.1 17.5.0-preview-2-33117-317 17.5.274-preview 4.6.0-2.23113.15 @@ -96,6 +96,7 @@ 6.0.0-alpha.1.21072.5 $(Tooling_MicrosoftCodeAnalysisTestingVersion) + $(Tooling_MicrosoftCodeAnalysisTestingVersion) $(Tooling_MicrosoftCodeAnalysisTestingVersion) $(MicrosoftVisualStudioPackagesVersion) 0.1.149-beta diff --git a/src/Compiler/Directory.Packages.props b/src/Compiler/Directory.Packages.props index afb5b2d83eb..6b91bea3ddb 100644 --- a/src/Compiler/Directory.Packages.props +++ b/src/Compiler/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj index eaf51ec31f8..04cf3559c8e 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj @@ -17,6 +17,11 @@ + + + + + @@ -26,6 +31,7 @@ + diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index b2c2204cca5..7a8be538f3c 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -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; + 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"] = "

Hello world

", - }); - - 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"" -// -#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, ""

Hello world

""); - } - #pragma warning restore 1998 - } -} -#pragma warning restore 1591 -"); + TestState = + { + AdditionalFiles = + { + ("/0/Pages/Index.razor", "

Hello world

") + }, + }, + }; - 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? 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"); + + 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)); + } + } + } + + 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"); diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Resources/SourceGenerator_RazorFiles_Works/Pages_Index_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Resources/SourceGenerator_RazorFiles_Works/Pages_Index_razor.g.cs new file mode 100644 index 00000000000..c5129a923c4 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Resources/SourceGenerator_RazorFiles_Works/Pages_Index_razor.g.cs @@ -0,0 +1,22 @@ +#pragma checksum "/0/Pages/Index.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6b5db227a6aa2228c777b0771108b184b1fc5df3" +// +#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, "

Hello world

"); + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs new file mode 100644 index 00000000000..ba6768e1d13 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1+Test.cs @@ -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 + where TSourceGenerator : IIncrementalGenerator, new() + { + public class Test : CSharpSourceGeneratorTest + { + 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 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 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(); + 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; + } + } + } + + /// + /// Loads expected generated sources from embedded resources based on the test name. + /// + /// The current test method name. + /// The current instance. + 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); + } + } + } +} diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs new file mode 100644 index 00000000000..1096ec87cc3 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs @@ -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 + where TSourceGenerator : IIncrementalGenerator, new() + { + } +}