Skip to content

Commit 546bac4

Browse files
CopilotYunchuWangCopilot
authored
Expand Azure Functions smoke tests to cover source generator scenarios (#604)
* Initial plan * Add source generator smoke test scenario definitions Co-authored-by: YunchuWang <[email protected]> * Expand Azure Functions smoke tests for source generator coverage Co-authored-by: YunchuWang <[email protected]> * Update test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj Co-authored-by: Copilot <[email protected]> * Update test/AzureFunctionsSmokeTests/README.md Co-authored-by: Copilot <[email protected]> * Update test/AzureFunctionsSmokeTests/run-smoketests.ps1 Co-authored-by: Copilot <[email protected]> * Handle null checks in smoke validation Co-authored-by: YunchuWang <[email protected]> * Switch generator entity increment to CallEntityAsync Co-authored-by: YunchuWang <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: YunchuWang <[email protected]> Co-authored-by: wangbill <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent a6e1a8a commit 546bac4

File tree

4 files changed

+332
-2
lines changed

4 files changed

+332
-2
lines changed

test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
5+
<RootNamespace>AzureFunctionsSmokeTests</RootNamespace>
6+
<AssemblyName>AzureFunctionsSmokeTests</AssemblyName>
57
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
68
<OutputType>Exe</OutputType>
79
<Nullable>enable</Nullable>

test/AzureFunctionsSmokeTests/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The smoke tests ensure that:
1313
## Structure
1414

1515
- **HelloCitiesOrchestration.cs** - Simple orchestration that calls multiple activities
16+
- **SourceGeneratorScenarios.cs** - Class-based orchestration, activity, entity, and event coverage for source generator validation
1617
- **Program.cs** - Azure Functions host entry point
1718
- **host.json** - Azure Functions host configuration
1819
- **local.settings.json** - Local development settings
@@ -42,8 +43,8 @@ The script will:
4243
4. Start the Azure Functions app in a Docker container
4344
5. Trigger the HelloCities orchestration via HTTP
4445
6. Poll for orchestration completion
45-
7. Validate the result
46-
8. Clean up all containers
46+
7. Trigger the source generator orchestration and validate generated activity/entity/event/sub-orchestrator behaviors
47+
9. Clean up all containers
4748

4849
### Parameters
4950

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.IO;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using Microsoft.Azure.Functions.Worker;
8+
using Microsoft.Azure.Functions.Worker.Http;
9+
using Microsoft.DurableTask;
10+
using Microsoft.DurableTask.Client;
11+
using Microsoft.DurableTask.Entities;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace AzureFunctionsSmokeTests;
15+
16+
/// <summary>
17+
/// Input payload for the generated orchestration scenario.
18+
/// </summary>
19+
/// <param name="Name">The name to use when composing greetings.</param>
20+
public record GeneratorRequest([property: JsonPropertyName("name")] string? Name);
21+
22+
/// <summary>
23+
/// Output payload for the generated orchestration scenario.
24+
/// </summary>
25+
/// <param name="Greeting">The greeting text created by the activity function.</param>
26+
/// <param name="GreetingLength">The length of the generated greeting.</param>
27+
/// <param name="CounterTotal">The current total stored in the entity.</param>
28+
/// <param name="ChildMessage">The response returned by the child orchestrator.</param>
29+
/// <param name="EventMessage">The message carried by the durable event.</param>
30+
public record GeneratorResult(
31+
[property: JsonPropertyName("greeting")] string Greeting,
32+
[property: JsonPropertyName("greetingLength")] int GreetingLength,
33+
[property: JsonPropertyName("counterTotal")] int CounterTotal,
34+
[property: JsonPropertyName("childMessage")] string ChildMessage,
35+
[property: JsonPropertyName("eventMessage")] string EventMessage);
36+
37+
/// <summary>
38+
/// Durable event payload used by the generated orchestration.
39+
/// </summary>
40+
/// <param name="Message">The event message.</param>
41+
[DurableEvent("GeneratorSignal")]
42+
public record GeneratorSignal([property: JsonPropertyName("message")] string Message);
43+
44+
/// <summary>
45+
/// Entity state used by <see cref="GeneratorCounter"/>.
46+
/// </summary>
47+
public sealed class GeneratorCounterState
48+
{
49+
/// <summary>
50+
/// Gets or sets the running total tracked by the entity.
51+
/// </summary>
52+
public int Count { get; set; }
53+
}
54+
55+
/// <summary>
56+
/// Entity implementation used to validate source generator entity trigger output.
57+
/// </summary>
58+
[DurableTask(nameof(GeneratorCounter))]
59+
public sealed class GeneratorCounter : TaskEntity<GeneratorCounterState>
60+
{
61+
/// <summary>
62+
/// Increments the counter by the specified amount.
63+
/// </summary>
64+
/// <param name="context">The task entity context.</param>
65+
/// <param name="amount">The amount to add.</param>
66+
public void Add(TaskEntityContext context, int amount)
67+
{
68+
this.State.Count += amount;
69+
}
70+
71+
/// <summary>
72+
/// Gets the current counter value.
73+
/// </summary>
74+
/// <returns>The current counter total.</returns>
75+
public int GetCount()
76+
{
77+
return this.State.Count;
78+
}
79+
80+
/// <inheritdoc/>
81+
protected override GeneratorCounterState InitializeState(TaskEntityOperation entityOperation)
82+
{
83+
return new GeneratorCounterState();
84+
}
85+
}
86+
87+
/// <summary>
88+
/// Activity used to validate source generator activity trigger output.
89+
/// </summary>
90+
[DurableTask(nameof(CountCharactersActivity))]
91+
public sealed class CountCharactersActivity : TaskActivity<string, int>
92+
{
93+
/// <inheritdoc/>
94+
public override Task<int> RunAsync(TaskActivityContext context, string input)
95+
{
96+
return Task.FromResult(input?.Length ?? 0);
97+
}
98+
}
99+
100+
/// <summary>
101+
/// Child orchestrator used to validate generated sub-orchestration call methods.
102+
/// </summary>
103+
[DurableTask(nameof(ChildGeneratedOrchestration))]
104+
public sealed class ChildGeneratedOrchestration : TaskOrchestrator<int, string>
105+
{
106+
/// <inheritdoc/>
107+
public override Task<string> RunAsync(TaskOrchestrationContext context, int input)
108+
{
109+
return Task.FromResult($"Child processed {input}");
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Primary orchestration that exercises the Durable Task source generator output for Azure Functions.
115+
/// </summary>
116+
[DurableTask(nameof(GeneratedOrchestration))]
117+
public sealed class GeneratedOrchestration : TaskOrchestrator<GeneratorRequest?, GeneratorResult>
118+
{
119+
internal const string DefaultName = "SourceGen";
120+
121+
/// <inheritdoc/>
122+
public override async Task<GeneratorResult> RunAsync(TaskOrchestrationContext context, GeneratorRequest? input)
123+
{
124+
string name = string.IsNullOrWhiteSpace(input?.Name) ? DefaultName : input!.Name!;
125+
126+
// Function-based activity trigger call using generated extension.
127+
string greeting = await context.CallComposeGreetingAsync(name);
128+
129+
// Class-based activity call using generated extension and activity trigger generated by source generator.
130+
int length = await context.CallCountCharactersActivityAsync(greeting);
131+
132+
// Entity trigger generated by source generator.
133+
EntityInstanceId counterId = new EntityInstanceId(nameof(GeneratorCounter), context.InstanceId);
134+
await context.Entities.CallEntityAsync(counterId, "Add", length);
135+
int total = await context.Entities.CallEntityAsync<int>(counterId, "GetCount");
136+
137+
// Durable event extensions generated by source generator.
138+
context.SendGeneratorSignal(context.InstanceId, new GeneratorSignal($"Processed {name}"));
139+
GeneratorSignal confirmation = await context.WaitForGeneratorSignalAsync();
140+
141+
// Sub-orchestration call using generated extension methods.
142+
string childMessage = await context.CallChildGeneratedOrchestrationAsync(length);
143+
144+
return new GeneratorResult(greeting, length, total, childMessage, confirmation.Message);
145+
}
146+
}
147+
148+
/// <summary>
149+
/// HTTP trigger and auxiliary functions used to start source generator scenarios.
150+
/// </summary>
151+
public static class GeneratorFunctions
152+
{
153+
/// <summary>
154+
/// Composes a greeting string. Generates an activity trigger via source generators.
155+
/// </summary>
156+
/// <param name="name">The name to greet.</param>
157+
/// <returns>The greeting text.</returns>
158+
[Function(nameof(ComposeGreeting))]
159+
public static string ComposeGreeting([ActivityTrigger] string name)
160+
{
161+
return $"Hello, {name}!";
162+
}
163+
164+
/// <summary>
165+
/// Starts the generated orchestration using a generated scheduling extension method.
166+
/// </summary>
167+
/// <param name="req">The HTTP request.</param>
168+
/// <param name="client">The durable client.</param>
169+
/// <param name="executionContext">The function execution context.</param>
170+
/// <returns>The HTTP response.</returns>
171+
[Function("GeneratedOrchestration_HttpStart")]
172+
public static async Task<HttpResponseData> StartGeneratedOrchestrationAsync(
173+
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
174+
[DurableClient] DurableTaskClient client,
175+
FunctionContext executionContext)
176+
{
177+
ILogger logger = executionContext.GetLogger("GeneratedOrchestration_HttpStart");
178+
179+
GeneratorRequest? request = await TryReadRequestAsync(req);
180+
string instanceId = await client.ScheduleNewGeneratedOrchestrationInstanceAsync(
181+
request ?? new GeneratorRequest(GeneratedOrchestration.DefaultName));
182+
183+
logger.LogInformation("Started generated orchestration with ID = '{InstanceId}'.", instanceId);
184+
return client.CreateCheckStatusResponse(req, instanceId);
185+
}
186+
187+
static async Task<GeneratorRequest?> TryReadRequestAsync(HttpRequestData req)
188+
{
189+
if (req.Body.CanSeek)
190+
{
191+
req.Body.Seek(0, SeekOrigin.Begin);
192+
}
193+
194+
using StreamReader reader = new(req.Body);
195+
string body = await reader.ReadToEndAsync();
196+
if (string.IsNullOrWhiteSpace(body))
197+
{
198+
return null;
199+
}
200+
201+
try
202+
{
203+
return JsonSerializer.Deserialize<GeneratorRequest>(body);
204+
}
205+
catch (JsonException)
206+
{
207+
return null;
208+
}
209+
}
210+
}

test/AzureFunctionsSmokeTests/run-smoketests.ps1

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,123 @@ try {
252252
throw "Orchestration did not complete within timeout period"
253253
}
254254

255+
Write-Host ""
256+
257+
# Step 9: Trigger source generator orchestration
258+
Write-Host "Step 9: Triggering source generator orchestration..." -ForegroundColor Green
259+
$generatorStartUrl = "http://localhost:$Port/api/GeneratedOrchestration_HttpStart"
260+
261+
try {
262+
$generatorStartResponse = Invoke-WebRequest -Uri $generatorStartUrl -Method Post -UseBasicParsing
263+
if ($generatorStartResponse.StatusCode -ne 202) {
264+
throw "Unexpected status code: $($generatorStartResponse.StatusCode)"
265+
}
266+
}
267+
catch {
268+
Write-Host "Failed to trigger source generator orchestration. Error: $_" -ForegroundColor Red
269+
Write-Host "Container logs:" -ForegroundColor Yellow
270+
docker logs $ContainerName
271+
throw
272+
}
273+
274+
$generatorResponse = $generatorStartResponse.Content | ConvertFrom-Json
275+
$generatorStatusQuery = $generatorResponse.statusQueryGetUri
276+
$generatorInstanceId = $generatorResponse.id
277+
278+
Write-Host "Source generator orchestration started with instance ID: $generatorInstanceId" -ForegroundColor Green
279+
Write-Host "Status query URI: $generatorStatusQuery" -ForegroundColor Cyan
280+
Write-Host ""
281+
282+
# Step 10: Poll for completion and validate source generator output
283+
Write-Host "Step 10: Polling for source generator orchestration completion..." -ForegroundColor Green
284+
$generatorStartTime = Get-Date
285+
$generatorCompleted = $false
286+
$generatorConsecutiveErrors = 0
287+
288+
while (((Get-Date) - $generatorStartTime).TotalSeconds -lt $Timeout) {
289+
Start-Sleep -Seconds 2
290+
291+
try {
292+
$generatorStatusResponse = Invoke-WebRequest -Uri $generatorStatusQuery -UseBasicParsing
293+
$generatorStatus = $generatorStatusResponse.Content | ConvertFrom-Json
294+
$generatorConsecutiveErrors = 0
295+
296+
Write-Host "Current status: $($generatorStatus.runtimeStatus)" -ForegroundColor Yellow
297+
298+
if ($generatorStatus.runtimeStatus -eq "Completed") {
299+
$generatorCompleted = $true
300+
$output = $generatorStatus.output
301+
Write-Host ""
302+
Write-Host "Source generator orchestration completed successfully!" -ForegroundColor Green
303+
Write-Host "Output: $output" -ForegroundColor Cyan
304+
305+
$greeting = $output.greeting
306+
if (-not $greeting) { $greeting = $output.Greeting }
307+
308+
$greetingLength = $output.greetingLength
309+
if ($null -eq $greetingLength) { $greetingLength = $output.GreetingLength }
310+
311+
$counterTotal = $output.counterTotal
312+
if ($null -eq $counterTotal) { $counterTotal = $output.CounterTotal }
313+
314+
$childMessage = $output.childMessage
315+
if (-not $childMessage) { $childMessage = $output.ChildMessage }
316+
317+
$eventMessage = $output.eventMessage
318+
if (-not $eventMessage) { $eventMessage = $output.EventMessage }
319+
320+
if ($greeting -ne "Hello, SourceGen!") {
321+
throw "Unexpected greeting from generated activity: $greeting"
322+
}
323+
324+
if ($null -eq $greetingLength) {
325+
throw "Greeting length was not populated correctly: $greetingLength"
326+
}
327+
328+
$greetingLengthValue = [int]$greetingLength
329+
330+
if ($null -eq $counterTotal) {
331+
throw "Entity counter total was not populated correctly: $counterTotal"
332+
}
333+
334+
$counterTotalValue = [int]$counterTotal
335+
336+
if ($counterTotalValue -ne $greetingLengthValue) {
337+
throw "Entity counter total $counterTotalValue does not match greeting length $greetingLengthValue"
338+
}
339+
340+
if (-not $childMessage -or ($childMessage -notlike "Child processed *")) {
341+
throw "Unexpected child orchestration response: $childMessage"
342+
}
343+
344+
if (-not $eventMessage -or ($eventMessage -notlike "Processed*")) {
345+
throw "Unexpected durable event message: $eventMessage"
346+
}
347+
348+
break
349+
}
350+
elseif ($generatorStatus.runtimeStatus -eq "Failed" -or $generatorStatus.runtimeStatus -eq "Terminated") {
351+
throw "Orchestration ended with status: $($generatorStatus.runtimeStatus)"
352+
}
353+
}
354+
catch {
355+
$generatorConsecutiveErrors++
356+
Write-Host "Error polling generator orchestration (attempt $generatorConsecutiveErrors/3): $_" -ForegroundColor Red
357+
358+
if ($generatorConsecutiveErrors -ge 3) {
359+
Write-Host "Container logs:" -ForegroundColor Yellow
360+
docker logs $ContainerName
361+
throw "Too many consecutive errors polling source generator orchestration status"
362+
}
363+
}
364+
}
365+
366+
if (-not $generatorCompleted) {
367+
Write-Host "Container logs:" -ForegroundColor Yellow
368+
docker logs $ContainerName
369+
throw "Source generator orchestration did not complete within timeout period"
370+
}
371+
255372
Write-Host ""
256373
Write-Host "=== Smoke test completed successfully! ===" -ForegroundColor Green
257374
}

0 commit comments

Comments
 (0)