Skip to content

Commit f7a943b

Browse files
authored
Merge branch 'release/10.0.1xx' into darc-release/10.0.1xx-d5e70d62-80a9-432c-8bad-50dceb349e82
2 parents b4112da + 24c7ba4 commit f7a943b

24 files changed

+563
-85
lines changed

eng/Version.Details.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ This file should be imported by eng/Versions.props
140140
<!-- dotnet/core-setup dependencies -->
141141
<NETStandardLibraryRefPackageVersion>2.1.0</NETStandardLibraryRefPackageVersion>
142142
<!-- microsoft/testfx dependencies -->
143-
<MicrosoftTestingPlatformPackageVersion>1.9.0-preview.25451.2</MicrosoftTestingPlatformPackageVersion>
144-
<MSTestPackageVersion>3.11.0-preview.25451.2</MSTestPackageVersion>
143+
<MicrosoftTestingPlatformPackageVersion>1.9.0-preview.25452.4</MicrosoftTestingPlatformPackageVersion>
144+
<MSTestPackageVersion>3.11.0-preview.25452.4</MSTestPackageVersion>
145145
</PropertyGroup>
146146
<!--Property group for alternate package version names-->
147147
<PropertyGroup>

eng/Version.Details.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,13 +553,13 @@
553553
<Uri>https://github.com/dotnet/dotnet</Uri>
554554
<Sha>7ac1ca67bb1fb8a381c1c94a9f82a97725f0ccf3</Sha>
555555
</Dependency>
556-
<Dependency Name="Microsoft.Testing.Platform" Version="1.9.0-preview.25451.2">
556+
<Dependency Name="Microsoft.Testing.Platform" Version="1.9.0-preview.25452.4">
557557
<Uri>https://github.com/microsoft/testfx</Uri>
558-
<Sha>8d29291af97543579834e9109768c5ee89f63638</Sha>
558+
<Sha>0d5aa69d6b2cbeebff30536a22d31f4bd7ecf684</Sha>
559559
</Dependency>
560-
<Dependency Name="MSTest" Version="3.11.0-preview.25451.2">
560+
<Dependency Name="MSTest" Version="3.11.0-preview.25452.4">
561561
<Uri>https://github.com/microsoft/testfx</Uri>
562-
<Sha>8d29291af97543579834e9109768c5ee89f63638</Sha>
562+
<Sha>0d5aa69d6b2cbeebff30536a22d31f4bd7ecf684</Sha>
563563
</Dependency>
564564
<Dependency Name="Microsoft.Extensions.Configuration.Ini" Version="10.0.0-rc.2.25427.104">
565565
<Uri>https://github.com/dotnet/dotnet</Uri>

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2648,4 +2648,22 @@ Proceed?</value>
26482648
<data name="CmdMinimumExpectedTestsDescription" xml:space="preserve">
26492649
<value>Specifies the minimum number of tests that are expected to run.</value>
26502650
</data>
2651+
<data name="DotnetTestPipeOverlapping" xml:space="preserve">
2652+
<value>'dotnet' unexpectedly received overlapping messages from the 'dotnet test' named pipe.</value>
2653+
</data>
2654+
<data name="DotnetTestPipeIncompleteSize" xml:space="preserve">
2655+
<value>'dotnet' unexpectedly received less than 4 bytes from the 'dotnet test' named pipe.</value>
2656+
</data>
2657+
<data name="InternalLoopAsyncDidNotExitSuccessfullyErrorMessage" xml:space="preserve">
2658+
<value>Method '{0}' did not exit successfully</value>
2659+
</data>
2660+
<data name="DotnetTestPipeFailureHasHandshake" xml:space="preserve">
2661+
<value>Error disposing 'NamedPipeServer' corresponding to handshake:</value>
2662+
</data>
2663+
<data name="DotnetTestPipeFailureWithoutHandshake" xml:space="preserve">
2664+
<value>Error disposing 'NamedPipeServer', and no handshake was found.</value>
2665+
</data>
2666+
<data name="DotnetTestIncompatibleHandshakeVersion" xml:space="preserve">
2667+
<value>Supported protocol versions sent by Microsoft.Testing.Platform are '{0}'. The SDK supports '{1}', which is incompatible.</value>
2668+
</data>
26512669
</root>

src/Cli/dotnet/Commands/Test/CliConstants.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ internal static class HandshakeMessagePropertyNames
6565

6666
internal static class ProtocolConstants
6767
{
68-
internal const string Version = "1.0.0";
68+
/// <summary>
69+
/// The protocol versions that are supported by the current SDK. Multiple versions can be present and be semicolon separated.
70+
/// </summary>
71+
internal const string SupportedVersions = "1.0.0";
6972
}
7073

7174
internal static class ProjectProperties

src/Cli/dotnet/Commands/Test/CustomEventArgs.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
55

66
namespace Microsoft.DotNet.Cli.Commands.Test;
77

8-
internal class HandshakeArgs : EventArgs
8+
internal sealed class HandshakeArgs : EventArgs
99
{
1010
public Handshake Handshake { get; set; }
11+
public bool GotSupportedVersion { get; set; }
1112
}
1213

13-
internal class HelpEventArgs : EventArgs
14+
internal sealed class HelpEventArgs : EventArgs
1415
{
1516
public string ModulePath { get; set; }
1617

1718
public CommandLineOption[] CommandLineOptions { get; set; }
1819
}
1920

20-
internal class DiscoveredTestEventArgs : EventArgs
21+
internal sealed class DiscoveredTestEventArgs : EventArgs
2122
{
2223
public string ExecutionId { get; set; }
2324

@@ -26,7 +27,7 @@ internal class DiscoveredTestEventArgs : EventArgs
2627
public DiscoveredTest[] DiscoveredTests { get; set; }
2728
}
2829

29-
internal class TestResultEventArgs : EventArgs
30+
internal sealed class TestResultEventArgs : EventArgs
3031
{
3132
public string ExecutionId { get; set; }
3233

@@ -37,7 +38,7 @@ internal class TestResultEventArgs : EventArgs
3738
public FailedTestResult[] FailedTestResults { get; set; }
3839
}
3940

40-
internal class FileArtifactEventArgs : EventArgs
41+
internal sealed class FileArtifactEventArgs : EventArgs
4142
{
4243
public string ExecutionId { get; set; }
4344

@@ -46,25 +47,19 @@ internal class FileArtifactEventArgs : EventArgs
4647
public FileArtifact[] FileArtifacts { get; set; }
4748
}
4849

49-
internal class SessionEventArgs : EventArgs
50+
internal sealed class SessionEventArgs : EventArgs
5051
{
5152
public TestSession SessionEvent { get; set; }
5253
}
5354

54-
internal class ErrorEventArgs : EventArgs
55+
internal sealed class ErrorEventArgs : EventArgs
5556
{
5657
public string ErrorMessage { get; set; }
5758
}
5859

59-
internal class TestProcessExitEventArgs : EventArgs
60+
internal sealed class TestProcessExitEventArgs : EventArgs
6061
{
6162
public List<string> OutputData { get; set; }
6263
public List<string> ErrorData { get; set; }
6364
public int ExitCode { get; set; }
6465
}
65-
66-
internal class ExecutionEventArgs : EventArgs
67-
{
68-
public string ModulePath { get; set; }
69-
public string ExecutionId { get; set; }
70-
}

src/Cli/dotnet/Commands/Test/IPC/NamedPipeServer.cs

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
#nullable disable
55

66
using System.Buffers;
7+
using System.Diagnostics;
78
using System.Globalization;
89
using System.IO.Pipes;
910

1011
namespace Microsoft.DotNet.Cli.Commands.Test.IPC;
1112

1213
internal sealed class NamedPipeServer : NamedPipeBase
1314
{
14-
private readonly Func<IRequest, Task<IResponse>> _callback;
15+
private readonly Func<NamedPipeServer, IRequest, Task<IResponse>> _callback;
1516
private readonly NamedPipeServerStream _namedPipeServerStream;
1617
private readonly CancellationToken _cancellationToken;
1718
private readonly MemoryStream _serializationBuffer = new();
@@ -24,12 +25,12 @@ internal sealed class NamedPipeServer : NamedPipeBase
2425

2526
public NamedPipeServer(
2627
PipeNameDescription pipeNameDescription,
27-
Func<IRequest, Task<IResponse>> callback,
28+
Func<NamedPipeServer, IRequest, Task<IResponse>> callback,
2829
int maxNumberOfServerInstances,
2930
CancellationToken cancellationToken,
3031
bool skipUnknownMessages)
3132
{
32-
_namedPipeServerStream = new((PipeName = pipeNameDescription).Name, PipeDirection.InOut, maxNumberOfServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
33+
_namedPipeServerStream = new((PipeName = pipeNameDescription).Name, PipeDirection.InOut, maxNumberOfServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
3334
_callback = callback;
3435
_cancellationToken = cancellationToken;
3536
_skipUnknownMessages = skipUnknownMessages;
@@ -55,14 +56,6 @@ public async Task WaitConnectionAsync(CancellationToken cancellationToken)
5556
// We are being cancelled, so we don't need to wait anymore
5657
return;
5758
}
58-
catch (Exception ex)
59-
{
60-
// TODO: it might be better idea to let this exception bubble to
61-
// TestApplication. There, we should handle it by printing the exception without failing
62-
// the whole dotnet process, and provide more info about the specific test app that failed.
63-
// Potentially even knowing whether it's test host or test host controller that failed?
64-
Environment.FailFast($"[NamedPipeServer] Unhandled exception:{Environment.NewLine}{ex}", ex);
65-
}
6659
}, cancellationToken);
6760
}
6861

@@ -94,6 +87,11 @@ private async Task InternalLoopAsync(CancellationToken cancellationToken)
9487
if (currentMessageSize == 0)
9588
{
9689
// We need to read the message size, first 4 bytes
90+
if (currentReadBytes < sizeof(int))
91+
{
92+
throw new UnreachableException(CliCommandStrings.DotnetTestPipeIncompleteSize);
93+
}
94+
9795
currentMessageSize = BitConverter.ToInt32(_readBuffer, 0);
9896
missingBytesToReadOfCurrentChunk = currentReadBytes - sizeof(int);
9997
missingBytesToReadOfWholeMessage = currentMessageSize;
@@ -107,6 +105,11 @@ private async Task InternalLoopAsync(CancellationToken cancellationToken)
107105
missingBytesToReadOfWholeMessage -= missingBytesToReadOfCurrentChunk;
108106
}
109107

108+
if (missingBytesToReadOfWholeMessage < 0)
109+
{
110+
throw new UnreachableException(CliCommandStrings.DotnetTestPipeOverlapping);
111+
}
112+
110113
// If we have read all the message, we can deserialize it
111114
if (missingBytesToReadOfWholeMessage == 0)
112115
{
@@ -124,7 +127,7 @@ private async Task InternalLoopAsync(CancellationToken cancellationToken)
124127
var deserializedObject = (IRequest)requestNamedPipeSerializer.Deserialize(_messageBuffer);
125128

126129
// Call the callback
127-
IResponse response = await _callback(deserializedObject);
130+
IResponse response = await _callback(this, deserializedObject);
128131

129132
// Write the message size
130133
_messageBuffer.Position = 0;
@@ -205,21 +208,28 @@ public void Dispose()
205208
return;
206209
}
207210

208-
if (WasConnected)
211+
try
209212
{
210-
// If the loop task is null at this point we have race condition, means that the task didn't start yet and we already dispose.
211-
// This is unexpected and we throw an exception.
212-
213-
// To close gracefully we need to ensure that the client closed the stream line 103.
214-
if (!_loopTask.Wait(TimeSpan.FromSeconds(90)))
213+
if (WasConnected)
215214
{
216-
throw new InvalidOperationException("InternalLoopAsyncDidNotExitSuccessfullyErrorMessage");
215+
// If the loop task is null at this point we have race condition, means that the task didn't start yet and we already dispose.
216+
// This is unexpected and we throw an exception.
217+
218+
// To close gracefully we need to ensure that the client closed the stream line 103.
219+
if (!_loopTask.Wait(TimeSpan.FromSeconds(90)))
220+
{
221+
throw new InvalidOperationException(CliCommandStrings.InternalLoopAsyncDidNotExitSuccessfullyErrorMessage);
222+
}
217223
}
218224
}
225+
finally
226+
{
227+
// Ensure we are still disposing the resouces correctly, even if _loopTask completes with
228+
// an exception, or if the task doesn't complete within the 90 seconds limit.
229+
_namedPipeServerStream.Dispose();
230+
PipeName.Dispose();
219231

220-
_namedPipeServerStream.Dispose();
221-
PipeName.Dispose();
222-
223-
_disposed = true;
232+
_disposed = true;
233+
}
224234
}
225235
}

src/Cli/dotnet/Commands/Test/TestApplication.cs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class TestApplication(TestModule module, BuildOptions buildOptio
2323

2424
private Task _testAppPipeConnectionLoop;
2525
private readonly List<NamedPipeServer> _testAppPipeConnections = [];
26+
private readonly Dictionary<NamedPipeServer, HandshakeMessage> _handshakes = new();
2627

2728
public event EventHandler<HandshakeArgs> HandshakeReceived;
2829
public event EventHandler<HelpEventArgs> HelpRequested;
@@ -35,6 +36,8 @@ internal sealed class TestApplication(TestModule module, BuildOptions buildOptio
3536

3637
public TestModule Module { get; } = module;
3738

39+
public bool HasFailureDuringDispose { get; private set; }
40+
3841
public async Task<int> RunAsync(TestOptions testOptions)
3942
{
4043
if (testOptions.HasFilterMode && !ModulePathExists())
@@ -146,7 +149,7 @@ private async Task WaitConnectionAsync(CancellationToken token)
146149
{
147150
while (!token.IsCancellationRequested)
148151
{
149-
NamedPipeServer pipeConnection = new(_pipeNameDescription, OnRequest, NamedPipeServerStream.MaxAllowedServerInstances, token, skipUnknownMessages: true);
152+
var pipeConnection = new NamedPipeServer(_pipeNameDescription, OnRequest, NamedPipeServerStream.MaxAllowedServerInstances, token, skipUnknownMessages: true);
150153
pipeConnection.RegisterAllSerializers();
151154

152155
await pipeConnection.WaitConnectionAsync(token);
@@ -173,20 +176,17 @@ private async Task WaitConnectionAsync(CancellationToken token)
173176
}
174177
}
175178

176-
private Task<IResponse> OnRequest(IRequest request)
179+
private Task<IResponse> OnRequest(NamedPipeServer server, IRequest request)
177180
{
178181
try
179182
{
180183
switch (request)
181184
{
182185
case HandshakeMessage handshakeMessage:
183-
if (handshakeMessage.Properties.TryGetValue(HandshakeMessagePropertyNames.ModulePath, out string value))
184-
{
185-
OnHandshakeMessage(handshakeMessage);
186-
187-
return Task.FromResult((IResponse)CreateHandshakeMessage(GetSupportedProtocolVersion(handshakeMessage)));
188-
}
189-
break;
186+
_handshakes.Add(server, handshakeMessage);
187+
string negotiatedVersion = GetSupportedProtocolVersion(handshakeMessage);
188+
OnHandshakeMessage(handshakeMessage, negotiatedVersion.Length > 0);
189+
return Task.FromResult((IResponse)CreateHandshakeMessage(negotiatedVersion));
190190

191191
case CommandLineOptionMessages commandLineOptionMessages:
192192
OnCommandLineOptionMessages(commandLineOptionMessages);
@@ -236,15 +236,26 @@ private Task<IResponse> OnRequest(IRequest request)
236236

237237
private static string GetSupportedProtocolVersion(HandshakeMessage handshakeMessage)
238238
{
239-
handshakeMessage.Properties.TryGetValue(HandshakeMessagePropertyNames.SupportedProtocolVersions, out string protocolVersions);
239+
if (!handshakeMessage.Properties.TryGetValue(HandshakeMessagePropertyNames.SupportedProtocolVersions, out string protocolVersions) ||
240+
protocolVersions is null)
241+
{
242+
// It's not expected we hit this.
243+
// TODO: Maybe we should fail more hard?
244+
return string.Empty;
245+
}
240246

241-
string version = string.Empty;
242-
if (protocolVersions is not null && protocolVersions.Split(";").Contains(ProtocolConstants.Version))
247+
// NOTE: Today, ProtocolConstants.Version is only 1.0.0 (i.e, SDK supports only a single version).
248+
// Whenever we support multiple versions in SDK, we should do intersection
249+
// between protocolVersions given by MTP, and the versions supported by SDK.
250+
// Then we return the "highest" version from the intersection.
251+
// The current logic **assumes** that ProtocolConstants.SupportedVersions is a single version.
252+
if (protocolVersions.Split(";").Contains(ProtocolConstants.SupportedVersions))
243253
{
244-
version = ProtocolConstants.Version;
254+
return ProtocolConstants.SupportedVersions;
245255
}
246256

247-
return version;
257+
// The version given by MTP is not supported by SDK.
258+
return string.Empty;
248259
}
249260

250261
private static HandshakeMessage CreateHandshakeMessage(string version) =>
@@ -305,9 +316,9 @@ private bool ModulePathExists()
305316
return true;
306317
}
307318

308-
public void OnHandshakeMessage(HandshakeMessage handshakeMessage)
319+
public void OnHandshakeMessage(HandshakeMessage handshakeMessage, bool gotSupportedVersion)
309320
{
310-
HandshakeReceived?.Invoke(this, new HandshakeArgs { Handshake = new Handshake(handshakeMessage.Properties) });
321+
HandshakeReceived?.Invoke(this, new HandshakeArgs { Handshake = new Handshake(handshakeMessage.Properties), GotSupportedVersion = gotSupportedVersion });
311322
}
312323

313324
public void OnCommandLineOptionMessages(CommandLineOptionMessages commandLineOptionMessages)
@@ -387,7 +398,35 @@ public void Dispose()
387398
{
388399
foreach (var namedPipeServer in _testAppPipeConnections)
389400
{
390-
namedPipeServer.Dispose();
401+
try
402+
{
403+
namedPipeServer.Dispose();
404+
}
405+
catch (Exception ex)
406+
{
407+
StringBuilder messageBuilder;
408+
if (_handshakes.TryGetValue(namedPipeServer, out var handshake))
409+
{
410+
messageBuilder = new StringBuilder(CliCommandStrings.DotnetTestPipeFailureHasHandshake);
411+
messageBuilder.AppendLine();
412+
foreach (var kvp in handshake.Properties)
413+
{
414+
messageBuilder.AppendLine($"{kvp.Key}: {kvp.Value}");
415+
}
416+
}
417+
else
418+
{
419+
messageBuilder = new StringBuilder(CliCommandStrings.DotnetTestPipeFailureWithoutHandshake);
420+
messageBuilder.AppendLine();
421+
}
422+
423+
messageBuilder.AppendLine($"RunCommand: {Module.RunProperties.Command}");
424+
messageBuilder.AppendLine($"RunArguments: {Module.RunProperties.Arguments}");
425+
messageBuilder.AppendLine(ex.ToString());
426+
427+
HasFailureDuringDispose = true;
428+
Reporter.Error.WriteLine(messageBuilder.ToString());
429+
}
391430
}
392431

393432
WaitOnTestApplicationPipeConnectionLoop();

0 commit comments

Comments
 (0)