Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.AspNetCore.Http.Generators.Tests;

public class CompileTimeGeneratorTests : RequestDelegateGeneratorTests
{
protected override bool IsGeneratorEnabled { get; } = true;

[Fact]
public async Task MapAction_UnknownParameter_EmitsDiagnostic_NoSource()
{
// This will eventually be handled by the EndpointParameterSource.JsonBodyOrService.
// All parameters should theoretically be handleable with enough "Or"s in the future
// we'll remove this test and diagnostic.
var source = """
app.MapGet("/", (IServiceProvider provider) => "Hello world!");
""";
var expectedBody = "Hello world!";
var (generatorRunResult, compilation) = await RunGeneratorAsync(source);

// Emits diagnostic but generates no source
var result = Assert.IsType<GeneratorRunResult>(generatorRunResult);
var diagnostic = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticDescriptors.GetUnableToResolveParameterDescriptor("provider").Id, diagnostic.Id);
Assert.Empty(result.GeneratedSources);

// Falls back to runtime-generated endpoint
var endpoint = GetEndpointFromCompilation(compilation, false);

var httpContext = CreateHttpContext();
await endpoint.RequestDelegate(httpContext);
await VerifyResponseBodyAsync(httpContext, expectedBody);
}

[Fact]
public async Task MapGet_WithRequestDelegate_DoesNotGenerateSources()
{
var (generatorRunResult, compilation) = await RunGeneratorAsync("""
app.MapGet("/hello", (HttpContext context) => Task.CompletedTask);
""");
var results = Assert.IsType<GeneratorRunResult>(generatorRunResult);
Assert.Empty(GetStaticEndpoints(results, GeneratorSteps.EndpointModelStep));

var endpoint = GetEndpointFromCompilation(compilation, false);

var httpContext = CreateHttpContext();
await endpoint.RequestDelegate(httpContext);
await VerifyResponseBodyAsync(httpContext, "");
}

// Todo: Move this to a shared test that checks metadata once that is supported
// in the source generator.
[Theory]
[InlineData(@"app.MapGet(""/"", () => Console.WriteLine(""Returns void""));", null)]
[InlineData(@"app.MapGet(""/"", () => TypedResults.Ok(""Alright!""));", null)]
[InlineData(@"app.MapGet(""/"", () => Results.NotFound(""Oops!""));", null)]
[InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", "application/json")]
[InlineData(@"app.MapGet(""/"", () => ""Hello world!"");", "text/plain")]
public async Task MapAction_ProducesCorrectContentType(string source, string expectedContentType)
{
var (result, compilation) = await RunGeneratorAsync(source);

VerifyStaticEndpointModel(result, endpointModel =>
{
Assert.Equal("/", endpointModel.RoutePattern);
Assert.Equal("MapGet", endpointModel.HttpMethod);
Assert.Equal(expectedContentType, endpointModel.Response.ContentType);
});
}

[Fact]
public async Task MapAction_ExplicitRouteParamWithInvalidName_SimpleReturn()
{
var source = $$"""app.MapGet("/{routeValue}", ([FromRoute(Name = "invalidName" )] string parameterName) => parameterName);""";
var (_, compilation) = await RunGeneratorAsync(source);
var endpoint = GetEndpointFromCompilation(compilation);

var httpContext = CreateHttpContext();

var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => endpoint.RequestDelegate(httpContext));
Copy link
Member

Choose a reason for hiding this comment

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

RDF would just try to read context.Request.RouteValues["invalidName"] even though it's not part of the pattern, right? I know this PR is just about tests and not fixing behavior, but do we agree that we should align behavior here?

Copy link
Member Author

Choose a reason for hiding this comment

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

RDF throws the exception earlier when the endpoint is being constructed (ref). We could do something similar in RDG, but that would mean presenting the exception at compile-time which would require us to have a way to parse the route patterns in the generator. #46342 tracks doing this work.

Assert.Equal("'invalidName' is not a route parameter.", exception.Message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

namespace Microsoft.AspNetCore.Http.Generators.Tests;

public class RequestDelegateGeneratorIncrementalityTests : RequestDelegateGeneratorTestBase
public class CompileTimeIncrementalityTests : RequestDelegateGeneratorTestBase
{
protected override bool IsGeneratorEnabled { get; } = true;

[Fact]
public async Task MapAction_SameReturnType_DoesNotTriggerUpdate()
{
Expand Down Expand Up @@ -59,5 +61,5 @@ public async Task MapAction_ChangeBodyParamNullability_TriggersUpdate()
Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.New, value.Reason));
}

private static IEnumerable<(object Value, IncrementalStepRunReason Reason)> GetRunStepOutputs(GeneratorRunResult result) => result.TrackedOutputSteps.SelectMany(step => step.Value).SelectMany(value => value.Outputs);
private static IEnumerable<(object Value, IncrementalStepRunReason Reason)> GetRunStepOutputs(GeneratorRunResult? result) => result?.TrackedOutputSteps.SelectMany(step => step.Value).SelectMany(value => value.Outputs);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,30 @@

namespace Microsoft.AspNetCore.Http.Generators.Tests;

public class RequestDelegateGeneratorTestBase : LoggedTest
public abstract class RequestDelegateGeneratorTestBase : LoggedTest
{
internal static async Task<(GeneratorRunResult, Compilation)> RunGeneratorAsync(string sources, params string[] updatedSources)
protected abstract bool IsGeneratorEnabled { get; }

internal async Task<(GeneratorRunResult?, Compilation)> RunGeneratorAsync(string sources, params string[] updatedSources)
{
var compilation = await CreateCompilationAsync(sources);
var generator = new RequestDelegateGenerator().AsSourceGenerator();

// Enable the source generator in tests
// Return the compilation immediately if
// the generator is not enabled.
if (!IsGeneratorEnabled)
{
return (null, compilation);
}

// Configure the generator driver and run
// the compilation with it if the generator
// is enabled.
var generator = new RequestDelegateGenerator().AsSourceGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generators: new[]
{
generator
},
driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true));

// Run the source generator
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation,
out var _);
foreach (var updatedSource in updatedSources)
Expand Down Expand Up @@ -72,13 +81,30 @@ internal static StaticRouteHandlerModel.Endpoint[] GetStaticEndpoints(GeneratorR
return Array.Empty<StaticRouteHandlerModel.Endpoint>();
}

internal static Endpoint GetEndpointFromCompilation(Compilation compilation, bool expectSourceKey = true) =>
Assert.Single(GetEndpointsFromCompilation(compilation, expectSourceKey));
internal static void VerifyStaticEndpointModel(GeneratorRunResult? result, Action<StaticRouteHandlerModel.Endpoint> runAssertions)
{
if (result.HasValue)
{
runAssertions(GetStaticEndpoint(result.Value, GeneratorSteps.EndpointModelStep));
}
}

internal static Endpoint[] GetEndpointsFromCompilation(Compilation compilation, bool expectSourceKey = true)
internal static void VerifyStaticEndpointModels(GeneratorRunResult? result, Action<StaticRouteHandlerModel.Endpoint[]> runAssertions)
{
if (result.HasValue)
{
runAssertions(GetStaticEndpoints(result.Value, GeneratorSteps.EndpointModelStep));
}
}

internal Endpoint GetEndpointFromCompilation(Compilation compilation, bool? expectSourceKeyOverride = null) =>
Assert.Single(GetEndpointsFromCompilation(compilation, expectSourceKeyOverride));

internal Endpoint[] GetEndpointsFromCompilation(Compilation compilation, bool? expectSourceKeyOverride = null)
{
var assemblyName = compilation.AssemblyName!;
var symbolsName = Path.ChangeExtension(assemblyName, "pdb");
var expectSourceKey = (expectSourceKeyOverride ?? true) && IsGeneratorEnabled;

var output = new MemoryStream();
var pdb = new MemoryStream();
Expand Down Expand Up @@ -353,6 +379,10 @@ private static Task<Compilation> CreateCompilationAsync(string sources)

internal async Task VerifyAgainstBaselineUsingFile(Compilation compilation, [CallerMemberName] string callerName = "")
{
if (!IsGeneratorEnabled)
{
return;
}
var baselineFilePath = Path.Combine("RequestDelegateGenerator", "Baselines", $"{callerName}.generated.txt");
var generatedSyntaxTree = compilation.SyntaxTrees.Last();
var generatedCode = await generatedSyntaxTree.GetTextAsync();
Expand Down
Loading