Skip to content
Merged
3 changes: 3 additions & 0 deletions test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TopLevelNamespace>AzureFunctionsSmokeTests</TopLevelNamespace>
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
<RootNamespace>AzureFunctionsSmokeTests</RootNamespace>
<AssemblyName>AzureFunctionsSmokeTests</AssemblyName>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
Expand Down
3 changes: 2 additions & 1 deletion test/AzureFunctionsSmokeTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment thread
YunchuWang marked this conversation as resolved.
Outdated

### Parameters
Expand Down
210 changes: 210 additions & 0 deletions test/AzureFunctionsSmokeTests/SourceGeneratorScenarios.cs
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!;
Comment thread
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);
Comment thread
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;
}
}
}
117 changes: 117 additions & 0 deletions test/AzureFunctionsSmokeTests/run-smoketests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

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

The validation logic checks if greetingLength is falsy before throwing an error. However, zero (0) is a valid length value but evaluates to false in PowerShell. If the greeting were an empty string, the length would be 0, which would incorrectly trigger the error "Greeting length was not populated correctly: 0". Consider checking for null or using -eq $null instead of -not to avoid false positives with zero values.

Suggested change
if (-not $greetingLength) {
if ($null -eq $greetingLength) {

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

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.

throw "Greeting length was not populated correctly: $greetingLength"
}

$greetingLengthValue = [int]$greetingLength

if (-not $counterTotal) {
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
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
}
Expand Down
Loading