-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Ensure dotnet test messages are received in the right timings/modes #50727
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 |
---|---|---|
@@ -1,11 +1,10 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
#nullable disable | ||
|
||
using System.Diagnostics; | ||
using System.Globalization; | ||
using System.IO.Pipes; | ||
using System.Threading; | ||
using Microsoft.DotNet.Cli.Commands.Test.IPC; | ||
using Microsoft.DotNet.Cli.Commands.Test.IPC.Models; | ||
using Microsoft.DotNet.Cli.Commands.Test.IPC.Serializers; | ||
|
@@ -28,9 +27,7 @@ internal sealed class TestApplication( | |
private readonly List<string> _outputData = []; | ||
private readonly List<string> _errorData = []; | ||
private readonly string _pipeName = NamedPipeServer.GetPipeName(Guid.NewGuid().ToString("N")); | ||
private readonly CancellationTokenSource _cancellationToken = new(); | ||
|
||
private Task _testAppPipeConnectionLoop; | ||
private readonly List<NamedPipeServer> _testAppPipeConnections = []; | ||
private readonly Dictionary<NamedPipeServer, HandshakeMessage> _handshakes = new(); | ||
|
||
|
@@ -45,17 +42,26 @@ public async Task<int> RunAsync() | |
// Consider throwing an exception if it's called more than once. | ||
var processStartInfo = CreateProcessStartInfo(); | ||
|
||
_testAppPipeConnectionLoop = Task.Run(async () => await WaitConnectionAsync(_cancellationToken.Token), _cancellationToken.Token); | ||
var testProcessExitCode = await StartProcess(processStartInfo); | ||
var cancellationTokenSource = new CancellationTokenSource(); | ||
var cancellationToken = cancellationTokenSource.Token; | ||
Task? testAppPipeConnectionLoop = null; | ||
|
||
WaitOnTestApplicationPipeConnectionLoop(); | ||
try | ||
{ | ||
testAppPipeConnectionLoop = Task.Run(async () => await WaitConnectionAsync(cancellationToken), cancellationToken); | ||
var testProcessExitCode = await StartProcess(processStartInfo); | ||
if (_handler.HasMismatchingTestSessionEventCount()) | ||
{ | ||
throw new InvalidOperationException(CliCommandStrings.MissingTestSessionEnd); | ||
} | ||
|
||
if (_handler.HasMismatchingTestSessionEventCount()) | ||
return testProcessExitCode; | ||
} | ||
finally | ||
{ | ||
throw new InvalidOperationException(CliCommandStrings.MissingTestSessionEnd); | ||
cancellationTokenSource.Cancel(); | ||
testAppPipeConnectionLoop?.Wait((int)TimeSpan.FromSeconds(30).TotalMilliseconds); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we also, when would we expect a timeout? if test process exits, then this method should hopefully completely pretty much immediately There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following-up in #50733 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I go agree that it's expected for this task to complete almost immediately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I advice to keep always a timeout to avoid in case of bugs to hang forever and have a clear stack trace issue, when an api accept a time we should always set it. |
||
} | ||
|
||
return testProcessExitCode; | ||
} | ||
|
||
private ProcessStartInfo CreateProcessStartInfo() | ||
|
@@ -145,12 +151,6 @@ private string GetArguments() | |
return builder.ToString(); | ||
} | ||
|
||
private void WaitOnTestApplicationPipeConnectionLoop() | ||
{ | ||
_cancellationToken.Cancel(); | ||
_testAppPipeConnectionLoop?.Wait((int)TimeSpan.FromSeconds(30).TotalMilliseconds); | ||
} | ||
|
||
private async Task WaitConnectionAsync(CancellationToken token) | ||
{ | ||
try | ||
|
@@ -240,7 +240,7 @@ private Task<IResponse> OnRequest(NamedPipeServer server, IRequest request) | |
|
||
private static string GetSupportedProtocolVersion(HandshakeMessage handshakeMessage) | ||
{ | ||
if (!handshakeMessage.Properties.TryGetValue(HandshakeMessagePropertyNames.SupportedProtocolVersions, out string protocolVersions) || | ||
if (!handshakeMessage.Properties.TryGetValue(HandshakeMessagePropertyNames.SupportedProtocolVersions, out string? protocolVersions) || | ||
protocolVersions is null) | ||
{ | ||
// It's not expected we hit this. | ||
|
@@ -263,7 +263,7 @@ private static string GetSupportedProtocolVersion(HandshakeMessage handshakeMess | |
} | ||
|
||
private static HandshakeMessage CreateHandshakeMessage(string version) => | ||
new(new Dictionary<byte, string>(capacity: 5) | ||
new HandshakeMessage(new Dictionary<byte, string>(capacity: 5) | ||
{ | ||
{ HandshakeMessagePropertyNames.PID, Environment.ProcessId.ToString(CultureInfo.InvariantCulture) }, | ||
{ HandshakeMessagePropertyNames.Architecture, RuntimeInformation.ProcessArchitecture.ToString() }, | ||
|
@@ -276,7 +276,7 @@ private async Task<int> StartProcess(ProcessStartInfo processStartInfo) | |
{ | ||
Logger.LogTrace($"Test application arguments: {processStartInfo.Arguments}"); | ||
|
||
using var process = Process.Start(processStartInfo); | ||
using var process = Process.Start(processStartInfo)!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the maximum parallelism level for the dotnet CLI? have we tested with lots of test projects? we've seen There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We default to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @drognanar What was the fix for the threadpool starvation issue on VS side? What would be the right way for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used a combination of calling https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=net-9.0 to increase the threadpool size and specifically for the Process.Start it was put under an AsyncLock so that effectively one thread can enter this code path. this is because internally Process.Start runs under a lock on Windows and blocks a threadpool thread from running. |
||
StoreOutputAndErrorData(process); | ||
await process.WaitForExitAsync(); | ||
|
||
|
@@ -398,7 +398,5 @@ public void Dispose() | |
Reporter.Error.WriteLine(messageBuilder.ToString()); | ||
} | ||
} | ||
|
||
WaitOnTestApplicationPipeConnectionLoop(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,17 +46,18 @@ internal void OnHandshakeReceived(HandshakeMessage handshakeMessage, bool gotSup | |
return; | ||
} | ||
|
||
var executionId = handshakeMessage.Properties[HandshakeMessagePropertyNames.ExecutionId]; | ||
var arch = handshakeMessage.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower(); | ||
var tfm = TargetFrameworkParser.GetShortTargetFramework(handshakeMessage.Properties[HandshakeMessagePropertyNames.Framework]); | ||
var currentHandshakeInfo = (tfm, arch, executionId); | ||
|
||
if (!_handshakeInfo.HasValue) | ||
{ | ||
var executionId = handshakeMessage.Properties[HandshakeMessagePropertyNames.ExecutionId]; | ||
var arch = handshakeMessage.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower(); | ||
var tfm = TargetFrameworkParser.GetShortTargetFramework(handshakeMessage.Properties[HandshakeMessagePropertyNames.Framework]); | ||
|
||
_handshakeInfo = (tfm, arch, executionId); | ||
_handshakeInfo = currentHandshakeInfo; | ||
} | ||
else | ||
else if (_handshakeInfo.Value != currentHandshakeInfo) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. under what circumstances do we expect multiple handshakes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TestHostController+TestHost, or an orchestrator (e.g, Retry, or a future sharding orchestrator) |
||
{ | ||
// TODO: Verify we get the same info. | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.MismatchingHandshakeInfo, currentHandshakeInfo, _handshakeInfo.Value)); | ||
} | ||
|
||
var hostType = handshakeMessage.Properties[HandshakeMessagePropertyNames.HostType]; | ||
|
@@ -91,13 +92,14 @@ internal void OnDiscoveredTestsReceived(DiscoveredTestMessages discoveredTestMes | |
{ | ||
LogDiscoveredTests(discoveredTestMessages); | ||
|
||
// TODO: If _handshakeInfo is null, we should error. | ||
// We shouldn't be getting any discovered test messages without a previous handshake. | ||
|
||
if (_options.IsHelp) | ||
{ | ||
// TODO: Better to throw exception? | ||
return; | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageInHelpMode, nameof(DiscoveredTestMessages))); | ||
} | ||
|
||
if (!_handshakeInfo.HasValue) | ||
{ | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutHandshake, nameof(DiscoveredTestMessages))); | ||
} | ||
|
||
foreach (var test in discoveredTestMessages.DiscoveredMessages) | ||
|
@@ -112,13 +114,14 @@ internal void OnTestResultsReceived(TestResultMessages testResultMessage) | |
{ | ||
LogTestResults(testResultMessage); | ||
|
||
// TODO: If _handshakeInfo is null, we should error. | ||
// We shouldn't be getting any test result messages without a previous handshake. | ||
|
||
if (_options.IsHelp) | ||
{ | ||
// TODO: Better to throw exception? | ||
return; | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageInHelpMode, nameof(TestResultMessages))); | ||
} | ||
|
||
if (!_handshakeInfo.HasValue) | ||
{ | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutHandshake, nameof(TestResultMessages))); | ||
} | ||
|
||
var handshakeInfo = _handshakeInfo.Value; | ||
|
@@ -158,13 +161,14 @@ internal void OnFileArtifactsReceived(FileArtifactMessages fileArtifactMessages) | |
{ | ||
LogFileArtifacts(fileArtifactMessages); | ||
|
||
// TODO: If _handshakeInfo is null, we should error. | ||
// We shouldn't be getting any file artifact messages without a previous handshake. | ||
|
||
if (_options.IsHelp) | ||
{ | ||
// TODO: Better to throw exception? | ||
return; | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageInHelpMode, nameof(FileArtifactMessages))); | ||
} | ||
|
||
if (!_handshakeInfo.HasValue) | ||
{ | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutHandshake, nameof(FileArtifactMessages))); | ||
} | ||
|
||
var handshakeInfo = _handshakeInfo.Value; | ||
|
@@ -183,8 +187,12 @@ internal void OnSessionEventReceived(TestSessionEvent sessionEvent) | |
{ | ||
LogSessionEvent(sessionEvent); | ||
|
||
// TODO: If _handshakeInfo is null, we should error. | ||
// We shouldn't be getting any session event messages without a previous handshake. | ||
// TODO: Validate if we should get this message in help mode or not. | ||
|
||
if (!_handshakeInfo.HasValue) | ||
{ | ||
throw new InvalidOperationException(string.Format(CliCommandStrings.UnexpectedMessageWithoutHandshake, nameof(DiscoveredTestMessages))); | ||
} | ||
|
||
if (sessionEvent.SessionType == SessionEventTypes.TestSessionStart) | ||
{ | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
should we pass cancellationToken to this? it seems that once we start the process we do not cancel here?
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.
Good question. It's very possible that cancellation doesn't work very well today. @mariam-abdulla Do you know please?
Uh oh!
There was an error while loading. Please reload this page.
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.
Let's follow-up in a separate issue either way. #50732
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.
MTP handle the CTRL+C in a graceful way, here if I'm not mistaken we can only kill the process after the start and it's not graceful. So we can add a check on the boolean and don't start the process for "performance optimization" but we should avoid the kill.
Or add a client->server call(we're push only today) to signal the processes if we expect to "logically cancel" out of CTRL+C.