-
Notifications
You must be signed in to change notification settings - Fork 1.3k
.NET: Pre-build samples via tests to avoid timeouts #4245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -792,6 +792,9 @@ private async Task WaitForConditionAsync(Func<Task<bool>> condition, string mess | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async Task RunSampleTestAsync(string samplePath, 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -811,12 +814,39 @@ private async Task RunSampleTestAsync(string samplePath, Func<IReadOnlyList<Outp | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async Task BuildSampleAsync(string samplePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._outputHelper.WriteLine($"Building sample at {samplePath}..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ProcessStartInfo buildInfo = new() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileName = "dotnet", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Arguments = $"build -f {s_dotnetTargetFramework}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WorkingDirectory = samplePath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UseShellExecute = false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RedirectStandardOutput = true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RedirectStandardError = true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Process buildProcess = new() { StartInfo = buildInfo }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildProcess.Start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await buildProcess.WaitForExitAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (buildProcess.ExitCode != 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string stderr = await buildProcess.StandardError.ReadToEndAsync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new InvalidOperationException($"Failed to build sample at {samplePath}: {stderr}"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+835
to
+838
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (buildProcess.ExitCode != 0) | |
| { | |
| string stderr = await buildProcess.StandardError.ReadToEndAsync(); | |
| throw new InvalidOperationException($"Failed to build sample at {samplePath}: {stderr}"); | |
| string stdout = await buildProcess.StandardOutput.ReadToEndAsync(); | |
| string stderr = await buildProcess.StandardError.ReadToEndAsync(); | |
| if (buildProcess.ExitCode != 0) | |
| { | |
| StringBuilder messageBuilder = new(); | |
| messageBuilder.AppendLine($"Failed to build sample at {samplePath}."); | |
| messageBuilder.AppendLine($"Command: {buildInfo.FileName} {buildInfo.Arguments}"); | |
| messageBuilder.AppendLine($"WorkingDirectory: {buildInfo.WorkingDirectory}"); | |
| messageBuilder.AppendLine($"ExitCode: {buildProcess.ExitCode}"); | |
| if (!string.IsNullOrWhiteSpace(stdout)) | |
| { | |
| messageBuilder.AppendLine("StandardOutput:"); | |
| messageBuilder.AppendLine(stdout); | |
| } | |
| if (!string.IsNullOrWhiteSpace(stderr)) | |
| { | |
| messageBuilder.AppendLine("StandardError:"); | |
| messageBuilder.AppendLine(stderr); | |
| } | |
| throw new InvalidOperationException(messageBuilder.ToString()); |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BuildSampleAsync redirects stdout/stderr but never drains them; calling WaitForExitAsync() first can hang if the buffers fill (dotnet build can be chatty). Consider following the same pattern as RunCommandAsync (BeginOutputReadLine/BeginErrorReadLine) or reading both streams asynchronously before awaiting process exit, and add a timeout/cancellation token to avoid CI deadlocks.
| await buildProcess.WaitForExitAsync(); | |
| if (buildProcess.ExitCode != 0) | |
| { | |
| string stderr = await buildProcess.StandardError.ReadToEndAsync(); | |
| throw new InvalidOperationException($"Failed to build sample at {samplePath}: {stderr}"); | |
| // Begin reading stdout and stderr immediately to avoid deadlocks if buffers fill. | |
| Task<string> stdOutTask = buildProcess.StandardOutput.ReadToEndAsync(); | |
| Task<string> stdErrTask = buildProcess.StandardError.ReadToEndAsync(); | |
| // Apply a timeout to avoid CI deadlocks if the build hangs. | |
| TimeSpan buildTimeout = TimeSpan.FromMinutes(5); | |
| Task waitForExitTask = buildProcess.WaitForExitAsync(); | |
| Task completedTask = await Task.WhenAny(waitForExitTask, Task.Delay(buildTimeout)); | |
| if (completedTask != waitForExitTask) | |
| { | |
| // Timed out; try to kill the process and surface captured output. | |
| try | |
| { | |
| if (!buildProcess.HasExited) | |
| { | |
| buildProcess.Kill(entireProcessTree: true); | |
| } | |
| } | |
| catch | |
| { | |
| // Ignore failures when attempting to kill the process. | |
| } | |
| string timedOutStdOut = await stdOutTask; | |
| string timedOutStdErr = await stdErrTask; | |
| throw new TimeoutException( | |
| $"Timed out building sample at {samplePath} after {buildTimeout}. " + | |
| $"Stdout: {timedOutStdOut}{Environment.NewLine}Stderr: {timedOutStdErr}"); | |
| } | |
| // Ensure the process has fully exited and the streams are drained. | |
| await waitForExitTask; | |
| string stdOut = await stdOutTask; | |
| string stdErr = await stdErrTask; | |
| if (buildProcess.ExitCode != 0) | |
| { | |
| throw new InvalidOperationException( | |
| $"Failed to build sample at {samplePath} (exit code {buildProcess.ExitCode}): {stdErr}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BuildSampleAsync calls buildProcess.Start() without checking the return value. Elsewhere in this file (e.g., StartFunctionApp / RunCommandAsync) start failures are handled explicitly; doing the same here would produce a clearer failure instead of throwing later with less context.