// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests;
///
/// Shared test helpers for Azure Functions integration tests.
///
internal static class AzureFunctionsTestHelper
{
private static readonly TimeSpan s_buildTimeout = TimeSpan.FromMinutes(5);
///
/// Builds the sample project, failing fast if the build fails or times out.
///
internal static async Task BuildSampleAsync(
string samplePath,
string buildArgs,
ITestOutputHelper outputHelper)
{
outputHelper.WriteLine($"Building sample at {samplePath}...");
ProcessStartInfo buildInfo = new()
{
FileName = "dotnet",
Arguments = $"build {buildArgs}",
WorkingDirectory = samplePath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using Process buildProcess = new() { StartInfo = buildInfo };
buildProcess.Start();
// Read both streams asynchronously to avoid deadlocks from filled pipe buffers
Task stdoutTask = buildProcess.StandardOutput.ReadToEndAsync();
Task stderrTask = buildProcess.StandardError.ReadToEndAsync();
using CancellationTokenSource buildCts = new(s_buildTimeout);
try
{
await buildProcess.WaitForExitAsync(buildCts.Token);
}
catch (OperationCanceledException)
{
buildProcess.Kill(entireProcessTree: true);
throw new TimeoutException($"Build timed out after {s_buildTimeout.TotalMinutes} minutes for sample at {samplePath}.");
}
await Task.WhenAll(stdoutTask, stderrTask);
string stdout = stdoutTask.Result;
string stderr = stderrTask.Result;
if (buildProcess.ExitCode != 0)
{
throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}");
}
outputHelper.WriteLine($"Build completed for {samplePath}.");
}
///
/// Polls the Azure Functions host until it responds to an HTTP HEAD request,
/// failing fast if the host process exits unexpectedly.
///
internal static async Task WaitForFunctionsReadyAsync(
Process funcProcess,
string port,
HttpClient httpClient,
ITestOutputHelper outputHelper,
TimeSpan timeout,
string? samplePath = null)
{
outputHelper.WriteLine(
$"Waiting for Azure Functions Core Tools to be ready at http://localhost:{port}/...");
using CancellationTokenSource cts = new(timeout);
while (true)
{
// Fail fast if the host process has exited (e.g. build or startup failure)
if (funcProcess.HasExited)
{
string context = samplePath != null ? $" for sample '{samplePath}'" : string.Empty;
throw new InvalidOperationException(
$"The Azure Functions host process exited unexpectedly with code {funcProcess.ExitCode}{context}.");
}
try
{
using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{port}/");
using HttpResponseMessage response = await httpClient.SendAsync(request);
outputHelper.WriteLine($"Azure Functions Core Tools response: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
return;
}
}
catch (HttpRequestException)
{
// Expected when the app isn't yet ready
}
try
{
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
string context = samplePath != null ? $" for sample '{samplePath}'" : string.Empty;
throw new TimeoutException(
$"Timeout waiting for 'Azure Functions Core Tools is ready'{context}");
}
}
}
}