From c8ceab3da2e1c65ce6fb0e6592c68585f5b3b934 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:17:58 +0000 Subject: [PATCH 1/4] Reduce timeout flakiness for AzureFunctions Workflows samples run tests --- .../SamplesValidation.cs | 11 +++- .../WorkflowSamplesValidation.cs | 51 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs index c416fb6a2a..3832ec448d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs @@ -811,7 +811,7 @@ private async Task RunSampleTestAsync(string samplePath, Func logs) return process; } - private async Task WaitForAzureFunctionsAsync() + private async Task WaitForAzureFunctionsAsync(Process funcProcess) { this._outputHelper.WriteLine( $"Waiting for Azure Functions Core Tools to be ready at http://localhost:{AzureFunctionsPort}/..."); await this.WaitForConditionAsync( condition: async () => { + // Fail fast if the host process has exited (e.g. build or startup failure) + if (funcProcess.HasExited) + { + throw new InvalidOperationException( + $"The Azure Functions host process exited unexpectedly with code {funcProcess.ExitCode}."); + } + try { using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{AzureFunctionsPort}/"); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs index efb02b1aff..68f0a39531 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs @@ -36,7 +36,7 @@ public sealed class WorkflowSamplesValidation(ITestOutputHelper outputHelper) : private static bool s_infrastructureStarted; private static readonly TimeSpan s_orchestrationTimeout = TimeSpan.FromMinutes(1); - // In CI, `dotnet run` builds the Functions project from scratch before the host starts, so 60s is not enough. + // Timeout for the Azure Functions host to become ready after building. private static readonly TimeSpan s_functionsReadyTimeout = TimeSpan.FromSeconds(180); private static readonly string s_samplesPath = Path.GetFullPath( @@ -425,11 +425,15 @@ private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Messa private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Func, Task> testAction) { + // Build the sample project first (it may not have been built as part of the solution) + await this.BuildSampleAsync(samplePath); + + // Start the Azure Functions app List logsContainer = []; using Process funcProcess = this.StartFunctionApp(samplePath, logsContainer, requiresOpenAI); try { - await this.WaitForAzureFunctionsAsync(); + await this.WaitForAzureFunctionsAsync(funcProcess); await testAction(logsContainer); } finally @@ -438,12 +442,44 @@ private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Fu } } + private async Task BuildSampleAsync(string samplePath) + { + this._outputHelper.WriteLine($"Building sample at {samplePath}..."); + + ProcessStartInfo buildInfo = new() + { + FileName = "dotnet", + Arguments = $"build -f {s_dotnetTargetFramework} -c {BuildConfiguration}", + 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(); + await buildProcess.WaitForExitAsync(); + + string stderr = await stderrTask; + if (buildProcess.ExitCode != 0) + { + string stdout = await stdoutTask; + throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); + } + + this._outputHelper.WriteLine($"Build completed for {samplePath}."); + } + private Process StartFunctionApp(string samplePath, List logs, bool requiresOpenAI) { ProcessStartInfo startInfo = new() { FileName = "dotnet", - Arguments = $"run -f {s_dotnetTargetFramework} -c {BuildConfiguration} --port {AzureFunctionsPort}", + Arguments = $"run --no-build -f {s_dotnetTargetFramework} -c {BuildConfiguration} --port {AzureFunctionsPort}", WorkingDirectory = samplePath, UseShellExecute = false, RedirectStandardOutput = true, @@ -504,13 +540,20 @@ private Process StartFunctionApp(string samplePath, List logs, bool r return process; } - private async Task WaitForAzureFunctionsAsync() + private async Task WaitForAzureFunctionsAsync(Process funcProcess) { this._outputHelper.WriteLine( $"Waiting for Azure Functions Core Tools to be ready at http://localhost:{AzureFunctionsPort}/..."); await this.WaitForConditionAsync( condition: async () => { + // Fail fast if the host process has exited (e.g. build or startup failure) + if (funcProcess.HasExited) + { + throw new InvalidOperationException( + $"The Azure Functions host process exited unexpectedly with code {funcProcess.ExitCode}."); + } + try { using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{AzureFunctionsPort}/"); From 3ca572898628d5799c113a5d894e028840aec042 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:54:43 +0000 Subject: [PATCH 2/4] Add more updates --- .../ExternalClientTests.cs | 2 +- .../SamplesValidationBase.cs | 56 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs index 0e35d29750..6c200e9876 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs @@ -21,7 +21,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo { private static readonly TimeSpan s_defaultTimeout = Debugger.IsAttached ? TimeSpan.FromMinutes(5) - : TimeSpan.FromSeconds(30); + : TimeSpan.FromSeconds(60); private static readonly IConfiguration s_configuration = new ConfigurationBuilder() diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs index 5d541f614e..339b468664 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs @@ -141,6 +141,10 @@ protected async Task RunSampleTestAsync(string samplePath, Func logsContainer = []; using Process appProcess = this.StartConsoleApp(samplePath, logsContainer, uniqueTaskHubName); @@ -154,7 +158,11 @@ protected async Task RunSampleTestAsync(string samplePath, Func IsRedisRunningAsync() } } + private async Task BuildSampleAsync(string samplePath) + { + this.OutputHelper.WriteLine($"Building sample at {samplePath}..."); + + ProcessStartInfo buildInfo = new() + { + FileName = "dotnet", + Arguments = $"build --framework {DotnetTargetFramework}", + 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(); + await buildProcess.WaitForExitAsync(); + + string stderr = await stderrTask; + if (buildProcess.ExitCode != 0) + { + string stdout = await stdoutTask; + throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); + } + + this.OutputHelper.WriteLine($"Build completed for {samplePath}."); + } + private Process StartConsoleApp(string samplePath, BlockingCollection logs, string taskHubName) { ProcessStartInfo startInfo = new() { FileName = "dotnet", - Arguments = $"run --framework {DotnetTargetFramework}", + Arguments = $"run --no-build --framework {DotnetTargetFramework}", WorkingDirectory = samplePath, UseShellExecute = false, RedirectStandardOutput = true, @@ -360,11 +400,21 @@ void SetAndLogEnvironmentVariable(string key, string value) this.ConfigureAdditionalEnvironmentVariables(startInfo, SetAndLogEnvironmentVariable); - Process process = new() { StartInfo = startInfo }; + Process process = new() { StartInfo = startInfo, EnableRaisingEvents = true }; process.ErrorDataReceived += (sender, e) => this.HandleProcessOutput(e.Data, startInfo.FileName, "err", LogLevel.Error, logs); process.OutputDataReceived += (sender, e) => this.HandleProcessOutput(e.Data, startInfo.FileName, "out", LogLevel.Information, logs); + // When the process exits unexpectedly (e.g. build failure), complete the log collection + // so that ReadLogLine returns null immediately instead of blocking until the test timeout. + process.Exited += (sender, e) => + { + if (!logs.IsAddingCompleted) + { + logs.CompleteAdding(); + } + }; + if (!process.Start()) { throw new InvalidOperationException("Failed to start the console app"); From c39c3eea8b23e804cb9bb4307d84a13eaf7d70fd Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:11:33 +0000 Subject: [PATCH 3/4] Address PR comments --- .../SamplesValidationBase.cs | 6 +- .../AzureFunctionsTestHelper.cs | 105 ++++++++++++++++++ .../SamplesValidation.cs | 69 +----------- .../WorkflowSamplesValidation.cs | 68 +----------- 4 files changed, 117 insertions(+), 131 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs index 339b468664..be1948fd84 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs @@ -359,10 +359,12 @@ private async Task BuildSampleAsync(string samplePath) Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); await buildProcess.WaitForExitAsync(); - string stderr = await stderrTask; + await Task.WhenAll(stdoutTask, stderrTask); + + string stdout = stdoutTask.Result; + string stderr = stderrTask.Result; if (buildProcess.ExitCode != 0) { - string stdout = await stdoutTask; throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs new file mode 100644 index 0000000000..b4041061ab --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs @@ -0,0 +1,105 @@ +// 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 +{ + /// + /// Builds the sample project, failing fast if the build fails. + /// + 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(); + await buildProcess.WaitForExitAsync(); + + 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}"); + } + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs index 3832ec448d..b15f6e8f42 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs @@ -803,7 +803,8 @@ private async Task WaitForConditionAsync(Func> condition, string mess private async Task RunSampleTestAsync(string samplePath, Func, Task> testAction) { // Build the sample project first (it may not have been built as part of the solution) - await this.BuildSampleAsync(samplePath); + await AzureFunctionsTestHelper.BuildSampleAsync( + samplePath, $"-f {s_dotnetTargetFramework} -c {BuildConfiguration}", this._outputHelper); // Start the Azure Functions app List logsContainer = []; @@ -811,7 +812,8 @@ private async Task RunSampleTestAsync(string samplePath, Func stdoutTask = buildProcess.StandardOutput.ReadToEndAsync(); - Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); - await buildProcess.WaitForExitAsync(); - - string stderr = await stderrTask; - if (buildProcess.ExitCode != 0) - { - string stdout = await stdoutTask; - throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); - } - - this._outputHelper.WriteLine($"Build completed for {samplePath}."); - } - private Process StartFunctionApp(string samplePath, List logs) { ProcessStartInfo startInfo = new() @@ -919,37 +889,6 @@ private Process StartFunctionApp(string samplePath, List logs) return process; } - private async Task WaitForAzureFunctionsAsync(Process funcProcess) - { - this._outputHelper.WriteLine( - $"Waiting for Azure Functions Core Tools to be ready at http://localhost:{AzureFunctionsPort}/..."); - await this.WaitForConditionAsync( - condition: async () => - { - // Fail fast if the host process has exited (e.g. build or startup failure) - if (funcProcess.HasExited) - { - throw new InvalidOperationException( - $"The Azure Functions host process exited unexpectedly with code {funcProcess.ExitCode}."); - } - - try - { - using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{AzureFunctionsPort}/"); - using HttpResponseMessage response = await s_sharedHttpClient.SendAsync(request); - this._outputHelper.WriteLine($"Azure Functions Core Tools response: {response.StatusCode}"); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - // Expected when the app isn't yet ready - return false; - } - }, - message: "Azure Functions Core Tools is ready", - timeout: s_functionsReadyTimeout); - } - private async Task WaitForOrchestrationCompletionAsync(Uri statusUri) { using CancellationTokenSource timeoutCts = new(s_orchestrationTimeout); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs index 68f0a39531..da075ea107 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/WorkflowSamplesValidation.cs @@ -426,14 +426,16 @@ private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Messa private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Func, Task> testAction) { // Build the sample project first (it may not have been built as part of the solution) - await this.BuildSampleAsync(samplePath); + await AzureFunctionsTestHelper.BuildSampleAsync( + samplePath, $"-f {s_dotnetTargetFramework} -c {BuildConfiguration}", this._outputHelper); // Start the Azure Functions app List logsContainer = []; using Process funcProcess = this.StartFunctionApp(samplePath, logsContainer, requiresOpenAI); try { - await this.WaitForAzureFunctionsAsync(funcProcess); + await AzureFunctionsTestHelper.WaitForFunctionsReadyAsync( + funcProcess, AzureFunctionsPort, s_sharedHttpClient, this._outputHelper, s_functionsReadyTimeout, samplePath); await testAction(logsContainer); } finally @@ -442,38 +444,6 @@ private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Fu } } - private async Task BuildSampleAsync(string samplePath) - { - this._outputHelper.WriteLine($"Building sample at {samplePath}..."); - - ProcessStartInfo buildInfo = new() - { - FileName = "dotnet", - Arguments = $"build -f {s_dotnetTargetFramework} -c {BuildConfiguration}", - 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(); - await buildProcess.WaitForExitAsync(); - - string stderr = await stderrTask; - if (buildProcess.ExitCode != 0) - { - string stdout = await stdoutTask; - throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); - } - - this._outputHelper.WriteLine($"Build completed for {samplePath}."); - } - private Process StartFunctionApp(string samplePath, List logs, bool requiresOpenAI) { ProcessStartInfo startInfo = new() @@ -540,36 +510,6 @@ private Process StartFunctionApp(string samplePath, List logs, bool r return process; } - private async Task WaitForAzureFunctionsAsync(Process funcProcess) - { - this._outputHelper.WriteLine( - $"Waiting for Azure Functions Core Tools to be ready at http://localhost:{AzureFunctionsPort}/..."); - await this.WaitForConditionAsync( - condition: async () => - { - // Fail fast if the host process has exited (e.g. build or startup failure) - if (funcProcess.HasExited) - { - throw new InvalidOperationException( - $"The Azure Functions host process exited unexpectedly with code {funcProcess.ExitCode}."); - } - - try - { - using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{AzureFunctionsPort}/"); - using HttpResponseMessage response = await s_sharedHttpClient.SendAsync(request); - this._outputHelper.WriteLine($"Azure Functions Core Tools response: {response.StatusCode}"); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } - }, - message: "Azure Functions Core Tools is ready", - timeout: s_functionsReadyTimeout); - } - private async Task RunCommandAsync(string command, string[] args) { ProcessStartInfo startInfo = new() From 8bee5f5d338ea24998fd0485c5a1595fd2056c55 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:18:25 +0000 Subject: [PATCH 4/4] Address PR comments --- .../SamplesValidationBase.cs | 12 +++++++++++- .../AzureFunctionsTestHelper.cs | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs index be1948fd84..f5ecf0354d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs @@ -357,7 +357,17 @@ private async Task BuildSampleAsync(string samplePath) // Read both streams asynchronously to avoid deadlocks from filled pipe buffers Task stdoutTask = buildProcess.StandardOutput.ReadToEndAsync(); Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); - await buildProcess.WaitForExitAsync(); + + using CancellationTokenSource buildCts = new(TimeSpan.FromMinutes(5)); + try + { + await buildProcess.WaitForExitAsync(buildCts.Token); + } + catch (OperationCanceledException) + { + buildProcess.Kill(entireProcessTree: true); + throw new TimeoutException($"Build timed out after 5 minutes for sample at {samplePath}."); + } await Task.WhenAll(stdoutTask, stderrTask); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs index b4041061ab..b4150e6a58 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/AzureFunctionsTestHelper.cs @@ -9,8 +9,10 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests; /// internal static class AzureFunctionsTestHelper { + private static readonly TimeSpan s_buildTimeout = TimeSpan.FromMinutes(5); + /// - /// Builds the sample project, failing fast if the build fails. + /// Builds the sample project, failing fast if the build fails or times out. /// internal static async Task BuildSampleAsync( string samplePath, @@ -35,7 +37,17 @@ internal static async Task BuildSampleAsync( // Read both streams asynchronously to avoid deadlocks from filled pipe buffers Task stdoutTask = buildProcess.StandardOutput.ReadToEndAsync(); Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); - await buildProcess.WaitForExitAsync(); + + 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);