Skip to content
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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<!-- Several packages from the editor are used for testing HTML support, and share the following version. -->
<Tooling_HtmlEditorPackageVersion>17.5.101-preview-0002</Tooling_HtmlEditorPackageVersion>
<!-- Several packages share the MS.CA.Testing version -->
<Tooling_MicrosoftCodeAnalysisTestingVersion>1.1.2-beta1.22512.1</Tooling_MicrosoftCodeAnalysisTestingVersion>
<Tooling_MicrosoftCodeAnalysisTestingVersion>1.1.2-beta1.23115.1</Tooling_MicrosoftCodeAnalysisTestingVersion>
<MicrosoftVisualStudioShellPackagesVersion>17.5.0-preview-2-33117-317</MicrosoftVisualStudioShellPackagesVersion>
<MicrosoftVisualStudioPackagesVersion>17.5.274-preview</MicrosoftVisualStudioPackagesVersion>
<RoslynPackageVersion>4.6.0-2.23113.15</RoslynPackageVersion>
Expand All @@ -96,6 +96,7 @@
<MicrosoftNETSdkRazorPackageVersion>6.0.0-alpha.1.21072.5</MicrosoftNETSdkRazorPackageVersion>
<!-- Packages from dotnet/roslyn -->
<MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>$(Tooling_MicrosoftCodeAnalysisTestingVersion)</MicrosoftCodeAnalysisAnalyzerTestingPackageVersion>
<MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingXUnitPackageVersion>$(Tooling_MicrosoftCodeAnalysisTestingVersion)</MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingXUnitPackageVersion>
<MicrosoftCodeAnalysisTestingVerifiersXunitPackageVersion>$(Tooling_MicrosoftCodeAnalysisTestingVersion)</MicrosoftCodeAnalysisTestingVerifiersXunitPackageVersion>
<MicrosoftVisualStudioEditorPackageVersion>$(MicrosoftVisualStudioPackagesVersion)</MicrosoftVisualStudioEditorPackageVersion>
<MicrosoftVisualStudioExtensibilityTestingXunitVersion>0.1.149-beta</MicrosoftVisualStudioExtensibilityTestingXunitVersion>
Expand Down
1 change: 1 addition & 0 deletions src/Compiler/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCoreVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="$(MicrosoftCodeAnalysisCommonPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="$(MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingXUnitPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion)" />
<PackageVersion Include="Microsoft.CSharp" Version="$(MicrosoftCSharpVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
</ItemGroup>

<ItemGroup>
<Compile Remove="Resources\**\*.cs" />
<EmbeddedResource Include="Resources\**\*.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Build.Framework" />
Expand All @@ -26,6 +31,7 @@
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.extensibility.execution" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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""
Copy link
Member Author

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

// <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
Expand Down Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 This manually adds MetadataReference items for all the items that were being added to the old tests. The whole block was copied from CreateBaseProject().

}

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");
Expand Down
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"
Copy link
Member Author

@sharwell sharwell Feb 28, 2023

Choose a reason for hiding this comment

The 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()
{
}
}