From 149fe1585b8efc1f4969d62525bcfed626e134a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:32:04 +0000 Subject: [PATCH 1/8] Initial plan From 80e7d37f6bf155c06e3fae03c173fe1378e0bb97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:50:27 +0000 Subject: [PATCH 2/8] Add source generator smoke test scenario definitions Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../SourceGeneratorScenarios.cs | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs diff --git a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs new file mode 100644 index 00000000..72aaa85b --- /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 +{ + 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. + EntityId counterId = new EntityId(nameof(GeneratorCounter), context.InstanceId); + await context.SignalEntityAsync(counterId, "Add", length); + int total = await context.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(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; + } + } +} From 8d9a3a22082bd3b3f7f4b2ea670b7f515cdaaf41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:59:08 +0000 Subject: [PATCH 3/8] Expand Azure Functions smoke tests for source generator coverage Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../AzureFunctionsSmokeTests.csproj | 3 + test/AzureFunctionsSmokeTests/README.md | 3 +- .../SourceGeneratorScenarios.cs | 10 +- .../run-smoketests.ps1 | 117 ++++++++++++++++++ 4 files changed, 127 insertions(+), 6 deletions(-) diff --git a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj index 219d5bc9..8b59c6fc 100644 --- a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj +++ b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj @@ -2,6 +2,9 @@ net8.0 + AzureFunctionsSmokeTests + AzureFunctionsSmokeTests + AzureFunctionsSmokeTests v4 Exe enable diff --git a/test/AzureFunctionsSmokeTests/README.md b/test/AzureFunctionsSmokeTests/README.md index bfdf4241..527e4173 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,7 +43,7 @@ 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 +7. Trigger the source generator orchestration and validate generated activity/entity/event/sub-orchestrator behaviors 8. Clean up all containers ### Parameters diff --git a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs index 72aaa85b..193228e1 100644 --- a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs +++ b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs @@ -116,7 +116,7 @@ public override Task RunAsync(TaskOrchestrationContext context, int inpu [DurableTask(nameof(GeneratedOrchestration))] public sealed class GeneratedOrchestration : TaskOrchestrator { - const string DefaultName = "SourceGen"; + internal const string DefaultName = "SourceGen"; /// public override async Task RunAsync(TaskOrchestrationContext context, GeneratorRequest? input) @@ -130,9 +130,9 @@ public override async Task RunAsync(TaskOrchestrationContext co int length = await context.CallCountCharactersActivityAsync(greeting); // Entity trigger generated by source generator. - EntityId counterId = new EntityId(nameof(GeneratorCounter), context.InstanceId); - await context.SignalEntityAsync(counterId, "Add", length); - int total = await context.CallEntityAsync(counterId, "GetCount"); + EntityInstanceId counterId = new EntityInstanceId(nameof(GeneratorCounter), context.InstanceId); + await context.Entities.SignalEntityAsync(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}")); @@ -178,7 +178,7 @@ public static async Task StartGeneratedOrchestrationAsync( GeneratorRequest? request = await TryReadRequestAsync(req); string instanceId = await client.ScheduleNewGeneratedOrchestrationInstanceAsync( - request ?? new GeneratorRequest(DefaultName)); + request ?? new GeneratorRequest(GeneratedOrchestration.DefaultName)); logger.LogInformation("Started generated orchestration with ID = '{InstanceId}'.", instanceId); return client.CreateCheckStatusResponse(req, instanceId); diff --git a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 index 185b9e3d..304ff2e3 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 (-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) { + throw "Greeting length was not populated correctly: $greetingLength" + } + + $greetingLengthValue = [int]$greetingLength + + if (-not $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 } From 59594f6475960c2d0340dfbe89e5554c799e7b68 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 2 Jan 2026 17:06:38 -0800 Subject: [PATCH 4/8] Update test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj index 8b59c6fc..ccb904fc 100644 --- a/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj +++ b/test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj @@ -2,7 +2,6 @@ net8.0 - AzureFunctionsSmokeTests AzureFunctionsSmokeTests AzureFunctionsSmokeTests v4 From 015841501e7907221dff647624a0fde87f0894e2 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 2 Jan 2026 17:06:52 -0800 Subject: [PATCH 5/8] Update test/AzureFunctionsSmokeTests/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/AzureFunctionsSmokeTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AzureFunctionsSmokeTests/README.md b/test/AzureFunctionsSmokeTests/README.md index 527e4173..54cf761c 100644 --- a/test/AzureFunctionsSmokeTests/README.md +++ b/test/AzureFunctionsSmokeTests/README.md @@ -44,7 +44,7 @@ The script will: 5. Trigger the HelloCities orchestration via HTTP 6. Poll for orchestration completion 7. Trigger the source generator orchestration and validate generated activity/entity/event/sub-orchestrator behaviors -8. Clean up all containers +9. Clean up all containers ### Parameters From a348938284114777ae6bc2102d7bbb9f1736f9a4 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 2 Jan 2026 17:11:52 -0800 Subject: [PATCH 6/8] Update test/AzureFunctionsSmokeTests/run-smoketests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/AzureFunctionsSmokeTests/run-smoketests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 index 304ff2e3..7729d96c 100644 --- a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 +++ b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 @@ -327,7 +327,7 @@ try { $greetingLengthValue = [int]$greetingLength - if (-not $counterTotal) { + if ($null -eq $counterTotal) { throw "Entity counter total was not populated correctly: $counterTotal" } From 8afdd807351e57dbfbef735df256e9c5284ed6ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:15:02 +0000 Subject: [PATCH 7/8] Handle null checks in smoke validation Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- test/AzureFunctionsSmokeTests/run-smoketests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 index 7729d96c..542132b6 100644 --- a/test/AzureFunctionsSmokeTests/run-smoketests.ps1 +++ b/test/AzureFunctionsSmokeTests/run-smoketests.ps1 @@ -306,10 +306,10 @@ try { if (-not $greeting) { $greeting = $output.Greeting } $greetingLength = $output.greetingLength - if (-not $greetingLength) { $greetingLength = $output.GreetingLength } + if ($null -eq $greetingLength) { $greetingLength = $output.GreetingLength } $counterTotal = $output.counterTotal - if (-not $counterTotal) { $counterTotal = $output.CounterTotal } + if ($null -eq $counterTotal) { $counterTotal = $output.CounterTotal } $childMessage = $output.childMessage if (-not $childMessage) { $childMessage = $output.ChildMessage } @@ -321,7 +321,7 @@ try { throw "Unexpected greeting from generated activity: $greeting" } - if (-not $greetingLength) { + if ($null -eq $greetingLength) { throw "Greeting length was not populated correctly: $greetingLength" } From 410895f75615c80dab8ab148811ff6723a352c3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:19:40 +0000 Subject: [PATCH 8/8] Switch generator entity increment to CallEntityAsync Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs index 193228e1..0f09d1e2 100644 --- a/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs +++ b/test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs @@ -131,7 +131,7 @@ public override async Task RunAsync(TaskOrchestrationContext co // Entity trigger generated by source generator. EntityInstanceId counterId = new EntityInstanceId(nameof(GeneratorCounter), context.InstanceId); - await context.Entities.SignalEntityAsync(counterId, "Add", length); + await context.Entities.CallEntityAsync(counterId, "Add", length); int total = await context.Entities.CallEntityAsync(counterId, "GetCount"); // Durable event extensions generated by source generator.