diff --git a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj index 219d5bc9..ccb904fc 100644 --- a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj +++ b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj @@ -2,6 +2,8 @@ net8.0 + AzureFunctionsSmokeTests + AzureFunctionsSmokeTests v4 Exe enable diff --git a/test/AzureFunctionsSmokeTests/README.md b/test/AzureFunctionsSmokeTests/README.md index bfdf4241..54cf761c 100644 --- a/test/AzureFunctionsSmokeTests/README.md +++ b/test/AzureFunctionsSmokeTests/README.md @@ -13,6 +13,7 @@ The smoke tests ensure that: ## Structure - **HelloCitiesOrchestration.cs** - Simple orchestration that calls multiple activities +- **SourceGeneratorScenarios.cs** - Class-based orchestration, activity, entity, and event coverage for source generator validation - **Program.cs** - Azure Functions host entry point - **host.json** - Azure Functions host configuration - **local.settings.json** - Local development settings @@ -42,8 +43,8 @@ The script will: 4. Start the Azure Functions app in a Docker container 5. Trigger the HelloCities orchestration via HTTP 6. Poll for orchestration completion -7. Validate the result -8. Clean up all containers +7. Trigger the source generator orchestration and validate generated activity/entity/event/sub-orchestrator behaviors +9. Clean up all containers ### Parameters diff --git a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs new file mode 100644 index 00000000..0f09d1e2 --- /dev/null +++ b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs @@ -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; + +/// +/// Input payload for the generated orchestration scenario. +/// +/// The name to use when composing greetings. +public record GeneratorRequest([property: JsonPropertyName("name")] string? Name); + +/// +/// Output payload for the generated orchestration scenario. +/// +/// The greeting text created by the activity function. +/// The length of the generated greeting. +/// The current total stored in the entity. +/// The response returned by the child orchestrator. +/// The message carried by the durable event. +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); + +/// +/// Durable event payload used by the generated orchestration. +/// +/// The event message. +[DurableEvent("GeneratorSignal")] +public record GeneratorSignal([property: JsonPropertyName("message")] string Message); + +/// +/// Entity state used by . +/// +public sealed class GeneratorCounterState +{ + /// + /// Gets or sets the running total tracked by the entity. + /// + public int Count { get; set; } +} + +/// +/// Entity implementation used to validate source generator entity trigger output. +/// +[DurableTask(nameof(GeneratorCounter))] +public sealed class GeneratorCounter : TaskEntity +{ + /// + /// Increments the counter by the specified amount. + /// + /// The task entity context. + /// The amount to add. + public void Add(TaskEntityContext context, int amount) + { + this.State.Count += amount; + } + + /// + /// Gets the current counter value. + /// + /// The current counter total. + public int GetCount() + { + return this.State.Count; + } + + /// + protected override GeneratorCounterState InitializeState(TaskEntityOperation entityOperation) + { + return new GeneratorCounterState(); + } +} + +/// +/// Activity used to validate source generator activity trigger output. +/// +[DurableTask(nameof(CountCharactersActivity))] +public sealed class CountCharactersActivity : TaskActivity +{ + /// + public override Task RunAsync(TaskActivityContext context, string input) + { + return Task.FromResult(input?.Length ?? 0); + } +} + +/// +/// Child orchestrator used to validate generated sub-orchestration call methods. +/// +[DurableTask(nameof(ChildGeneratedOrchestration))] +public sealed class ChildGeneratedOrchestration : TaskOrchestrator +{ + /// + public override Task RunAsync(TaskOrchestrationContext context, int input) + { + return Task.FromResult($"Child processed {input}"); + } +} + +/// +/// Primary orchestration that exercises the Durable Task source generator output for Azure Functions. +/// +[DurableTask(nameof(GeneratedOrchestration))] +public sealed class GeneratedOrchestration : TaskOrchestrator +{ + internal const string DefaultName = "SourceGen"; + + /// + public override async Task RunAsync(TaskOrchestrationContext context, GeneratorRequest? input) + { + string name = string.IsNullOrWhiteSpace(input?.Name) ? DefaultName : input!.Name!; + + // 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.CallEntityAsync(counterId, "Add", length); + int total = await context.Entities.CallEntityAsync(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); + } +} + +/// +/// HTTP trigger and auxiliary functions used to start source generator scenarios. +/// +public static class GeneratorFunctions +{ + /// + /// Composes a greeting string. Generates an activity trigger via source generators. + /// + /// The name to greet. + /// The greeting text. + [Function(nameof(ComposeGreeting))] + public static string ComposeGreeting([ActivityTrigger] string name) + { + return $"Hello, {name}!"; + } + + /// + /// Starts the generated orchestration using a generated scheduling extension method. + /// + /// The HTTP request. + /// The durable client. + /// The function execution context. + /// The HTTP response. + [Function("GeneratedOrchestration_HttpStart")] + public static async Task 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 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(body); + } + catch (JsonException) + { + return null; + } + } +} diff --git a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 index 185b9e3d..542132b6 100644 --- a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 +++ b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 @@ -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 ($null -eq $greetingLength) { $greetingLength = $output.GreetingLength } + + $counterTotal = $output.counterTotal + if ($null -eq $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 ($null -eq $greetingLength) { + throw "Greeting length was not populated correctly: $greetingLength" + } + + $greetingLengthValue = [int]$greetingLength + + if ($null -eq $counterTotal) { + throw "Entity counter total was not populated correctly: $counterTotal" + } + + $counterTotalValue = [int]$counterTotal + + if ($counterTotalValue -ne $greetingLengthValue) { + throw "Entity counter total $counterTotalValue does not match greeting length $greetingLengthValue" + } + + if (-not $childMessage -or ($childMessage -notlike "Child processed *")) { + throw "Unexpected child orchestration response: $childMessage" + } + + if (-not $eventMessage -or ($eventMessage -notlike "Processed*")) { + throw "Unexpected durable event message: $eventMessage" + } + + break + } + elseif ($generatorStatus.runtimeStatus -eq "Failed" -or $generatorStatus.runtimeStatus -eq "Terminated") { + throw "Orchestration ended with status: $($generatorStatus.runtimeStatus)" + } + } + catch { + $generatorConsecutiveErrors++ + Write-Host "Error polling generator orchestration (attempt $generatorConsecutiveErrors/3): $_" -ForegroundColor Red + + if ($generatorConsecutiveErrors -ge 3) { + Write-Host "Container logs:" -ForegroundColor Yellow + docker logs $ContainerName + throw "Too many consecutive errors polling source generator orchestration status" + } + } + } + + if (-not $generatorCompleted) { + Write-Host "Container logs:" -ForegroundColor Yellow + docker logs $ContainerName + throw "Source generator orchestration did not complete within timeout period" + } + Write-Host "" Write-Host "=== Smoke test completed successfully! ===" -ForegroundColor Green }