Skip to content
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

Listen to build manager events to know when build is complete #64366

Merged
merged 9 commits into from
Oct 3, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ static void Main(string[] args)

await TestServices.Editor.SetTextAsync(editorText, HangMitigatingCancellationToken);

var buildSummary = await TestServices.SolutionExplorer.BuildSolutionAsync(waitForBuildToFinish: true, HangMitigatingCancellationToken);
var buildSummary = await TestServices.SolutionExplorer.BuildSolutionAndWaitAsync(HangMitigatingCancellationToken);
Assert.Equal("========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========", buildSummary);

await TestServices.ErrorList.ShowBuildErrorsAsync(HangMitigatingCancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ static void Main(string[] args)
var target = await TestServices.ErrorList.NavigateToErrorListItemAsync(0, isPreview: false, shouldActivate: true, HangMitigatingCancellationToken);
Assert.Equal(expectedContents[0], target);
Assert.Equal(25, await TestServices.Editor.GetCaretPositionAsync(HangMitigatingCancellationToken));
await TestServices.SolutionExplorer.BuildSolutionAsync(waitForBuildToFinish: true, HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.BuildSolutionAndWaitAsync(HangMitigatingCancellationToken);
await TestServices.ErrorList.ShowErrorListAsync(HangMitigatingCancellationToken);
await TestServices.Workspace.WaitForAllAsyncOperationsAsync(new[] { FeatureAttribute.Workspace, FeatureAttribute.SolutionCrawler, FeatureAttribute.DiagnosticService, FeatureAttribute.ErrorSquiggles, FeatureAttribute.ErrorList }, HangMitigatingCancellationToken);
actualContents = await TestServices.ErrorList.GetErrorsAsync(HangMitigatingCancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,49 +511,35 @@ public async Task<string> GetFileContentsAsync(string projectName, string relati
}

/// <returns>
/// If <paramref name="waitForBuildToFinish"/> is <see langword="true"/>, returns the build status line, which generally looks something like this:
/// The summary line for the build, which generally looks something like this:
///
/// <code>
/// ========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
/// </code>
///
/// Otherwise, this method does not wait for the build to complete and returns <see langword="null"/>.
/// </returns>
public async Task<string?> BuildSolutionAsync(bool waitForBuildToFinish, CancellationToken cancellationToken)
public async Task<string> BuildSolutionAndWaitAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var buildOutputWindowPane = await GetBuildOutputWindowPaneAsync(cancellationToken);
buildOutputWindowPane.Clear();

await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd97CmdID.BuildSln, cancellationToken);
if (waitForBuildToFinish)
{
return await WaitForBuildToFinishAsync(buildOutputWindowPane, cancellationToken);
}

return null;
}

/// <inheritdoc cref="WaitForBuildToFinishAsync(IVsOutputWindowPane, CancellationToken)"/>
public async Task<string> WaitForBuildToFinishAsync(CancellationToken cancellationToken)
{
var buildOutputWindowPane = await GetBuildOutputWindowPaneAsync(cancellationToken);
return await WaitForBuildToFinishAsync(buildOutputWindowPane, cancellationToken);
}
var buildManager = await GetRequiredGlobalServiceAsync<SVsSolutionBuildManager, IVsSolutionBuildManager2>(cancellationToken);
using var solutionEvents = new UpdateSolutionEvents(buildManager);
var buildCompleteTaskCompletionSource = new TaskCompletionSource<bool>();

/// <returns>
/// The summary line for the build, which generally looks something like this:
///
/// <code>
/// ========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
/// </code>
/// </returns>
private async Task<string> WaitForBuildToFinishAsync(IVsOutputWindowPane buildOutputWindowPane, CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
void HandleUpdateSolutionDone() => buildCompleteTaskCompletionSource.SetResult(true);
solutionEvents.OnUpdateSolutionDone += HandleUpdateSolutionDone;
try
{
await TestServices.Shell.ExecuteCommandAsync(VSConstants.VSStd97CmdID.BuildSln, cancellationToken);

await KnownUIContexts.SolutionExistsAndNotBuildingAndNotDebuggingContext;
JoeRobich marked this conversation as resolved.
Show resolved Hide resolved
await buildCompleteTaskCompletionSource.Task;
}
finally
{
solutionEvents.OnUpdateSolutionDone -= HandleUpdateSolutionDone;
}
JoeRobich marked this conversation as resolved.
Show resolved Hide resolved

// Force the error list to update
ErrorHandler.ThrowOnFailure(buildOutputWindowPane.FlushToTaskList());
Expand All @@ -566,8 +552,17 @@ private async Task<string> WaitForBuildToFinishAsync(IVsOutputWindowPane buildOu
return string.Empty;
}

// The build summary line should be second to last in the output window
return lines[^2].Extent.GetText();
// Find the build summary line
for (var index = lines.Count - 1; index >= 0; index--)
{
var lineText = lines[index].Extent.GetText();
if (lineText.StartsWith("========== Build:"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (lineText.StartsWith("========== Build:"))
if (lineText.StartsWith("=========="))

Since that won't depend on localization anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but the reality is that every test that uses this method immediately Asserts the returned string equals an English build summary. Probably long term we should capture the success, failed, and canceled values returned by the UpdateSolutionEvents Done and return those instead of the build summary.

{
return lineText;
}
}

return string.Empty;
}

public async Task<IVsOutputWindowPane> GetBuildOutputWindowPaneAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -682,4 +677,45 @@ private static string CreateTemporaryPath()
});
}
}

internal sealed class UpdateSolutionEvents : IVsUpdateSolutionEvents, IDisposable
{
private uint _cookie;
private readonly IVsSolutionBuildManager2 _solutionBuildManager;

public event Action? OnUpdateSolutionDone;

internal UpdateSolutionEvents(IVsSolutionBuildManager2 solutionBuildManager)
{
ThreadHelper.ThrowIfNotOnUIThread();

_solutionBuildManager = solutionBuildManager;
ErrorHandler.ThrowOnFailure(solutionBuildManager.AdviseUpdateSolutionEvents(this, out _cookie));
}

int IVsUpdateSolutionEvents.UpdateSolution_Begin(ref int pfCancelUpdate) => VSConstants.E_NOTIMPL;
int IVsUpdateSolutionEvents.UpdateSolution_StartUpdate(ref int pfCancelUpdate) => VSConstants.E_NOTIMPL;
int IVsUpdateSolutionEvents.UpdateSolution_Cancel() => VSConstants.E_NOTIMPL;
int IVsUpdateSolutionEvents.OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy) => VSConstants.E_NOTIMPL;

int IVsUpdateSolutionEvents.UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
{
OnUpdateSolutionDone?.Invoke();
return 0;
}

void IDisposable.Dispose()
{
ThreadHelper.ThrowIfNotOnUIThread();

OnUpdateSolutionDone = null;

if (_cookie != 0)
{
var tempCookie = _cookie;
_cookie = 0;
ErrorHandler.ThrowOnFailure(_solutionBuildManager.UnadviseUpdateSolutionEvents(tempCookie));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ End Module

await TestServices.ErrorList.NavigateToErrorListItemAsync(0, isPreview: false, shouldActivate: true, HangMitigatingCancellationToken);
await TestServices.EditorVerifier.CaretPositionAsync(43, HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.BuildSolutionAsync(waitForBuildToFinish: true, HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.BuildSolutionAndWaitAsync(HangMitigatingCancellationToken);
await TestServices.ErrorList.ShowErrorListAsync(HangMitigatingCancellationToken);
await TestServices.Workspace.WaitForAllAsyncOperationsAsync(new[] { FeatureAttribute.Workspace, FeatureAttribute.SolutionCrawler, FeatureAttribute.DiagnosticService, FeatureAttribute.ErrorSquiggles, FeatureAttribute.ErrorList }, HangMitigatingCancellationToken);
actualContents = await TestServices.ErrorList.GetErrorsAsync(HangMitigatingCancellationToken);
Expand Down