Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ protected async Task RunSampleTestAsync(string samplePath, Func<Process, Blockin
{
string uniqueTaskHubName = $"{this.TaskHubPrefix}-{Guid.NewGuid():N}"[..^26];

// Build the sample project first so that build failures are caught immediately
// instead of silently failing inside 'dotnet run' and causing a timeout.
await this.BuildSampleAsync(samplePath);

using BlockingCollection<OutputLog> logsContainer = [];
using Process appProcess = this.StartConsoleApp(samplePath, logsContainer, uniqueTaskHubName);

Expand All @@ -154,7 +158,11 @@ protected async Task RunSampleTestAsync(string samplePath, Func<Process, Blockin
}
finally
{
logsContainer.CompleteAdding();
if (!logsContainer.IsAddingCompleted)
{
logsContainer.CompleteAdding();
}

await this.StopProcessAsync(appProcess);
}
}
Expand Down Expand Up @@ -329,12 +337,44 @@ private async Task<bool> 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<string> stdoutTask = buildProcess.StandardOutput.ReadToEndAsync();
Task<string> 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}.");
Comment thread
westey-m marked this conversation as resolved.
}

private Process StartConsoleApp(string samplePath, BlockingCollection<OutputLog> logs, string taskHubName)
{
ProcessStartInfo startInfo = new()
{
FileName = "dotnet",
Arguments = $"run --framework {DotnetTargetFramework}",
Arguments = $"run --no-build --framework {DotnetTargetFramework}",
WorkingDirectory = samplePath,
UseShellExecute = false,
RedirectStandardOutput = true,
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ private async Task RunSampleTestAsync(string samplePath, Func<IReadOnlyList<Outp
try
{
// Wait for the app to be ready
await this.WaitForAzureFunctionsAsync();
await this.WaitForAzureFunctionsAsync(funcProcess);

// Run the test
await testAction(logsContainer);
Expand Down Expand Up @@ -919,13 +919,20 @@ private Process StartFunctionApp(string samplePath, List<OutputLog> 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 () =>
{
Comment thread
westey-m marked this conversation as resolved.
Outdated
// 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}.");
}
Comment thread
westey-m marked this conversation as resolved.
Outdated

try
{
using HttpRequestMessage request = new(HttpMethod.Head, $"http://localhost:{AzureFunctionsPort}/");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -425,11 +425,15 @@ private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Messa

private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Func<IReadOnlyList<OutputLog>, 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<OutputLog> logsContainer = [];
using Process funcProcess = this.StartFunctionApp(samplePath, logsContainer, requiresOpenAI);
try
{
await this.WaitForAzureFunctionsAsync();
await this.WaitForAzureFunctionsAsync(funcProcess);
await testAction(logsContainer);
}
finally
Expand All @@ -438,12 +442,44 @@ private async Task RunSampleTestAsync(string samplePath, bool requiresOpenAI, Fu
}
}

private async Task BuildSampleAsync(string samplePath)
Comment thread
westey-m marked this conversation as resolved.
Outdated
Comment thread
westey-m marked this conversation as resolved.
Outdated
{
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<string> stdoutTask = buildProcess.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = buildProcess.StandardError.ReadToEndAsync();
await buildProcess.WaitForExitAsync();
Comment thread
westey-m marked this conversation as resolved.
Outdated

string stderr = await stderrTask;
if (buildProcess.ExitCode != 0)
{
string stdout = await stdoutTask;
Comment thread
westey-m marked this conversation as resolved.
Outdated
throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}");
}

this._outputHelper.WriteLine($"Build completed for {samplePath}.");
Comment thread
westey-m marked this conversation as resolved.
Outdated
}

private Process StartFunctionApp(string samplePath, List<OutputLog> 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,
Expand Down Expand Up @@ -504,13 +540,20 @@ private Process StartFunctionApp(string samplePath, List<OutputLog> 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}/");
Expand Down
Loading