-
Notifications
You must be signed in to change notification settings - Fork 56
Expand Azure Functions smoke tests to cover source generator scenarios #604
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
Changes from 3 commits
149fe15
80e7d37
8d9a3a2
59594f6
0158415
a348938
8afdd80
410895f
d8fde77
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 |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.IO; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Azure.Functions.Worker.Http; | ||
| using Microsoft.DurableTask; | ||
| using Microsoft.DurableTask.Client; | ||
| using Microsoft.DurableTask.Entities; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace AzureFunctionsSmokeTests; | ||
|
|
||
| /// <summary> | ||
| /// Input payload for the generated orchestration scenario. | ||
| /// </summary> | ||
| /// <param name="Name">The name to use when composing greetings.</param> | ||
| public record GeneratorRequest([property: JsonPropertyName("name")] string? Name); | ||
|
|
||
| /// <summary> | ||
| /// Output payload for the generated orchestration scenario. | ||
| /// </summary> | ||
| /// <param name="Greeting">The greeting text created by the activity function.</param> | ||
| /// <param name="GreetingLength">The length of the generated greeting.</param> | ||
| /// <param name="CounterTotal">The current total stored in the entity.</param> | ||
| /// <param name="ChildMessage">The response returned by the child orchestrator.</param> | ||
| /// <param name="EventMessage">The message carried by the durable event.</param> | ||
| public record GeneratorResult( | ||
| [property: JsonPropertyName("greeting")] string Greeting, | ||
| [property: JsonPropertyName("greetingLength")] int GreetingLength, | ||
| [property: JsonPropertyName("counterTotal")] int CounterTotal, | ||
| [property: JsonPropertyName("childMessage")] string ChildMessage, | ||
| [property: JsonPropertyName("eventMessage")] string EventMessage); | ||
|
|
||
| /// <summary> | ||
| /// Durable event payload used by the generated orchestration. | ||
| /// </summary> | ||
| /// <param name="Message">The event message.</param> | ||
| [DurableEvent("GeneratorSignal")] | ||
| public record GeneratorSignal([property: JsonPropertyName("message")] string Message); | ||
|
|
||
| /// <summary> | ||
| /// Entity state used by <see cref="GeneratorCounter"/>. | ||
| /// </summary> | ||
| public sealed class GeneratorCounterState | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the running total tracked by the entity. | ||
| /// </summary> | ||
| public int Count { get; set; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Entity implementation used to validate source generator entity trigger output. | ||
| /// </summary> | ||
| [DurableTask(nameof(GeneratorCounter))] | ||
| public sealed class GeneratorCounter : TaskEntity<GeneratorCounterState> | ||
| { | ||
| /// <summary> | ||
| /// Increments the counter by the specified amount. | ||
| /// </summary> | ||
| /// <param name="context">The task entity context.</param> | ||
| /// <param name="amount">The amount to add.</param> | ||
| public void Add(TaskEntityContext context, int amount) | ||
| { | ||
| this.State.Count += amount; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the current counter value. | ||
| /// </summary> | ||
| /// <returns>The current counter total.</returns> | ||
| public int GetCount() | ||
| { | ||
| return this.State.Count; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| protected override GeneratorCounterState InitializeState(TaskEntityOperation entityOperation) | ||
| { | ||
| return new GeneratorCounterState(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity used to validate source generator activity trigger output. | ||
| /// </summary> | ||
| [DurableTask(nameof(CountCharactersActivity))] | ||
| public sealed class CountCharactersActivity : TaskActivity<string, int> | ||
| { | ||
| /// <inheritdoc/> | ||
| public override Task<int> RunAsync(TaskActivityContext context, string input) | ||
| { | ||
| return Task.FromResult(input?.Length ?? 0); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Child orchestrator used to validate generated sub-orchestration call methods. | ||
| /// </summary> | ||
| [DurableTask(nameof(ChildGeneratedOrchestration))] | ||
| public sealed class ChildGeneratedOrchestration : TaskOrchestrator<int, string> | ||
| { | ||
| /// <inheritdoc/> | ||
| public override Task<string> RunAsync(TaskOrchestrationContext context, int input) | ||
| { | ||
| return Task.FromResult($"Child processed {input}"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Primary orchestration that exercises the Durable Task source generator output for Azure Functions. | ||
| /// </summary> | ||
| [DurableTask(nameof(GeneratedOrchestration))] | ||
| public sealed class GeneratedOrchestration : TaskOrchestrator<GeneratorRequest?, GeneratorResult> | ||
| { | ||
| internal const string DefaultName = "SourceGen"; | ||
|
|
||
| /// <inheritdoc/> | ||
| public override async Task<GeneratorResult> RunAsync(TaskOrchestrationContext context, GeneratorRequest? input) | ||
| { | ||
| string name = string.IsNullOrWhiteSpace(input?.Name) ? DefaultName : input!.Name!; | ||
|
YunchuWang marked this conversation as resolved.
|
||
|
|
||
| // Function-based activity trigger call using generated extension. | ||
| string greeting = await context.CallComposeGreetingAsync(name); | ||
|
|
||
| // Class-based activity call using generated extension and activity trigger generated by source generator. | ||
| int length = await context.CallCountCharactersActivityAsync(greeting); | ||
|
|
||
| // Entity trigger generated by source generator. | ||
| EntityInstanceId counterId = new EntityInstanceId(nameof(GeneratorCounter), context.InstanceId); | ||
| await context.Entities.SignalEntityAsync(counterId, "Add", length); | ||
|
YunchuWang marked this conversation as resolved.
Outdated
|
||
| int total = await context.Entities.CallEntityAsync<int>(counterId, "GetCount"); | ||
|
|
||
| // Durable event extensions generated by source generator. | ||
| context.SendGeneratorSignal(context.InstanceId, new GeneratorSignal($"Processed {name}")); | ||
| GeneratorSignal confirmation = await context.WaitForGeneratorSignalAsync(); | ||
|
|
||
| // Sub-orchestration call using generated extension methods. | ||
| string childMessage = await context.CallChildGeneratedOrchestrationAsync(length); | ||
|
|
||
| return new GeneratorResult(greeting, length, total, childMessage, confirmation.Message); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// HTTP trigger and auxiliary functions used to start source generator scenarios. | ||
| /// </summary> | ||
| public static class GeneratorFunctions | ||
| { | ||
| /// <summary> | ||
| /// Composes a greeting string. Generates an activity trigger via source generators. | ||
| /// </summary> | ||
| /// <param name="name">The name to greet.</param> | ||
| /// <returns>The greeting text.</returns> | ||
| [Function(nameof(ComposeGreeting))] | ||
| public static string ComposeGreeting([ActivityTrigger] string name) | ||
| { | ||
| return $"Hello, {name}!"; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Starts the generated orchestration using a generated scheduling extension method. | ||
| /// </summary> | ||
| /// <param name="req">The HTTP request.</param> | ||
| /// <param name="client">The durable client.</param> | ||
| /// <param name="executionContext">The function execution context.</param> | ||
| /// <returns>The HTTP response.</returns> | ||
| [Function("GeneratedOrchestration_HttpStart")] | ||
| public static async Task<HttpResponseData> StartGeneratedOrchestrationAsync( | ||
| [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, | ||
| [DurableClient] DurableTaskClient client, | ||
| FunctionContext executionContext) | ||
| { | ||
| ILogger logger = executionContext.GetLogger("GeneratedOrchestration_HttpStart"); | ||
|
|
||
| GeneratorRequest? request = await TryReadRequestAsync(req); | ||
| string instanceId = await client.ScheduleNewGeneratedOrchestrationInstanceAsync( | ||
| request ?? new GeneratorRequest(GeneratedOrchestration.DefaultName)); | ||
|
|
||
| logger.LogInformation("Started generated orchestration with ID = '{InstanceId}'.", instanceId); | ||
| return client.CreateCheckStatusResponse(req, instanceId); | ||
| } | ||
|
|
||
| static async Task<GeneratorRequest?> TryReadRequestAsync(HttpRequestData req) | ||
| { | ||
| if (req.Body.CanSeek) | ||
| { | ||
| req.Body.Seek(0, SeekOrigin.Begin); | ||
| } | ||
|
|
||
| using StreamReader reader = new(req.Body); | ||
| string body = await reader.ReadToEndAsync(); | ||
| if (string.IsNullOrWhiteSpace(body)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| return JsonSerializer.Deserialize<GeneratorRequest>(body); | ||
| } | ||
| catch (JsonException) | ||
| { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -252,6 +252,123 @@ try { | |||||
| throw "Orchestration did not complete within timeout period" | ||||||
| } | ||||||
|
|
||||||
| Write-Host "" | ||||||
|
|
||||||
| # Step 9: Trigger source generator orchestration | ||||||
| Write-Host "Step 9: Triggering source generator orchestration..." -ForegroundColor Green | ||||||
| $generatorStartUrl = "http://localhost:$Port/api/GeneratedOrchestration_HttpStart" | ||||||
|
|
||||||
| try { | ||||||
| $generatorStartResponse = Invoke-WebRequest -Uri $generatorStartUrl -Method Post -UseBasicParsing | ||||||
| if ($generatorStartResponse.StatusCode -ne 202) { | ||||||
| throw "Unexpected status code: $($generatorStartResponse.StatusCode)" | ||||||
| } | ||||||
| } | ||||||
| catch { | ||||||
| Write-Host "Failed to trigger source generator orchestration. Error: $_" -ForegroundColor Red | ||||||
| Write-Host "Container logs:" -ForegroundColor Yellow | ||||||
| docker logs $ContainerName | ||||||
| throw | ||||||
| } | ||||||
|
|
||||||
| $generatorResponse = $generatorStartResponse.Content | ConvertFrom-Json | ||||||
| $generatorStatusQuery = $generatorResponse.statusQueryGetUri | ||||||
| $generatorInstanceId = $generatorResponse.id | ||||||
|
|
||||||
| Write-Host "Source generator orchestration started with instance ID: $generatorInstanceId" -ForegroundColor Green | ||||||
| Write-Host "Status query URI: $generatorStatusQuery" -ForegroundColor Cyan | ||||||
| Write-Host "" | ||||||
|
|
||||||
| # Step 10: Poll for completion and validate source generator output | ||||||
| Write-Host "Step 10: Polling for source generator orchestration completion..." -ForegroundColor Green | ||||||
| $generatorStartTime = Get-Date | ||||||
| $generatorCompleted = $false | ||||||
| $generatorConsecutiveErrors = 0 | ||||||
|
|
||||||
| while (((Get-Date) - $generatorStartTime).TotalSeconds -lt $Timeout) { | ||||||
| Start-Sleep -Seconds 2 | ||||||
|
|
||||||
| try { | ||||||
| $generatorStatusResponse = Invoke-WebRequest -Uri $generatorStatusQuery -UseBasicParsing | ||||||
| $generatorStatus = $generatorStatusResponse.Content | ConvertFrom-Json | ||||||
| $generatorConsecutiveErrors = 0 | ||||||
|
|
||||||
| Write-Host "Current status: $($generatorStatus.runtimeStatus)" -ForegroundColor Yellow | ||||||
|
|
||||||
| if ($generatorStatus.runtimeStatus -eq "Completed") { | ||||||
| $generatorCompleted = $true | ||||||
| $output = $generatorStatus.output | ||||||
| Write-Host "" | ||||||
| Write-Host "Source generator orchestration completed successfully!" -ForegroundColor Green | ||||||
| Write-Host "Output: $output" -ForegroundColor Cyan | ||||||
|
|
||||||
| $greeting = $output.greeting | ||||||
| if (-not $greeting) { $greeting = $output.Greeting } | ||||||
|
|
||||||
| $greetingLength = $output.greetingLength | ||||||
| if (-not $greetingLength) { $greetingLength = $output.GreetingLength } | ||||||
|
|
||||||
| $counterTotal = $output.counterTotal | ||||||
| if (-not $counterTotal) { $counterTotal = $output.CounterTotal } | ||||||
|
|
||||||
| $childMessage = $output.childMessage | ||||||
| if (-not $childMessage) { $childMessage = $output.ChildMessage } | ||||||
|
|
||||||
| $eventMessage = $output.eventMessage | ||||||
| if (-not $eventMessage) { $eventMessage = $output.EventMessage } | ||||||
|
|
||||||
| if ($greeting -ne "Hello, SourceGen!") { | ||||||
| throw "Unexpected greeting from generated activity: $greeting" | ||||||
| } | ||||||
|
|
||||||
| if (-not $greetingLength) { | ||||||
|
||||||
| if (-not $greetingLength) { | |
| if ($null -eq $greetingLength) { |
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.
Updated the validation to use null checks so zero-length values are treated correctly. Fix is in commit 8afdd80.
Uh oh!
There was an error while loading. Please reload this page.