Skip to content

Commit 5354be1

Browse files
nohwndCopilot
andcommitted
feat: new async vstest client with STJ serialization
Initial implementation of a new async vstest client library for #15570. Key features: - Fully async APIs (Task-based, no blocking) - Reports errors from vstest.console process exits immediately - Non-blocking async I/O for child process output - Supports multiple concurrent sessions (no shared static state) - Uses System.Text.Json for serialization (no Newtonsoft.Json) - Minimal dependencies (ObjectModel + System.Text.Json) - Core operations: StartSession, EndSession, DiscoverTests, RunTests Architecture: - AsyncVsTestClient: main client orchestrating process + socket - AsyncTestSession: represents a live vstest.console connection - AsyncRequestSender: length-prefixed TCP protocol communication - ProcessManager: async process lifecycle management - TestObjectDeserializer: STJ-based property-bag deserialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1307468 commit 5354be1

16 files changed

Lines changed: 1873 additions & 0 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
using Microsoft.TestPlatform.Client.Async.Internal;
9+
10+
namespace Microsoft.TestPlatform.Client.Async;
11+
12+
/// <summary>
13+
/// Represents an active connection to a vstest.console process.
14+
/// Owns the process and socket connection for its lifetime.
15+
/// </summary>
16+
internal sealed class AsyncTestSession : IAsyncTestSession
17+
{
18+
internal readonly AsyncRequestSender Sender;
19+
internal readonly ProcessManager Process;
20+
private volatile bool _isConnected;
21+
22+
internal AsyncTestSession(AsyncRequestSender sender, ProcessManager process)
23+
{
24+
Sender = sender;
25+
Process = process;
26+
_isConnected = true;
27+
28+
// Monitor process exit to update IsConnected.
29+
_ = MonitorProcessAsync();
30+
}
31+
32+
/// <inheritdoc />
33+
public int ProcessId => Process.ProcessId;
34+
35+
/// <inheritdoc />
36+
public bool IsConnected => _isConnected && !Process.HasExited;
37+
38+
/// <inheritdoc />
39+
public async Task EndSessionAsync(CancellationToken cancellationToken)
40+
{
41+
if (!_isConnected) return;
42+
_isConnected = false;
43+
44+
try
45+
{
46+
await Sender.SendMessageAsync(ProtocolConstants.SessionEnd, cancellationToken).ConfigureAwait(false);
47+
}
48+
catch
49+
{
50+
// Process may have already exited.
51+
}
52+
53+
// Wait briefly for graceful exit.
54+
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
55+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
56+
57+
try
58+
{
59+
var cancelTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
60+
using var registration = linkedCts.Token.Register(() => cancelTcs.TrySetResult(true));
61+
await Task.WhenAny(Process.ExitedTask, cancelTcs.Task).ConfigureAwait(false);
62+
}
63+
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
64+
{
65+
// Timeout waiting for graceful exit — will be killed in Dispose.
66+
}
67+
}
68+
69+
/// <inheritdoc />
70+
public async ValueTask DisposeAsync()
71+
{
72+
await EndSessionAsync(CancellationToken.None).ConfigureAwait(false);
73+
Sender.Dispose();
74+
Process.Dispose();
75+
}
76+
77+
private async Task MonitorProcessAsync()
78+
{
79+
await Process.ExitedTask.ConfigureAwait(false);
80+
_isConnected = false;
81+
}
82+
}

0 commit comments

Comments
 (0)