Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions test/AzureFunctionsSmokeTests/AzureFunctionsSmokeTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>AzureFunctionsSmokeTests</RootNamespace>
<AssemblyName>AzureFunctionsSmokeTests</AssemblyName>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
Expand Down
5 changes: 3 additions & 2 deletions 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,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

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!;

// 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<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 ($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
}
Expand Down
Loading