Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add client-declared `IsStateful` capability to MTP by Evangelink · Pull Request #9789 · microsoft/testfx · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -225,6 +225,13 @@ interface InitializeParams {
// If true, the client supports the testing/testUpdates/attachments request.
attachmentsSupport: true,

// If true, the client is stateful: it persists an addressable set of test nodes for the
// whole session and keeps each node in its last-known state until it is explicitly updated
// (for example, an IDE test explorer). If false or missing, the client is stateless: it
// consumes test updates as a stream and does not retain node state after the run
// (for example, `dotnet test`). Defaults to false.
isStateful: true,

// If true, the client support a port to which child processes
// can connect to.
// Note: The test runner is expected to ensure the synchronization of messages
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ internal sealed class ConsoleTestHost(
: CommonHost(serviceProvider)
{
private static readonly ClientInfo ClientInfoHost = new("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version);
private static readonly IClientInfo ClientInfoService = new ClientInfoService("testingplatform-console", PlatformVersion.Version, new ClientCapabilitiesService(IsStateful: false));

private readonly ILogger<ConsoleTestHost> _logger = serviceProvider.GetLoggerFactory().CreateLogger<ConsoleTestHost>();
private readonly IClock _clock = serviceProvider.GetClock();
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ private async Task<object> HandleRequestCoreAsync(RequestMessage message, RpcInv

case (JsonRpcMethods.Initialize, InitializeRequestArgs args):
_client = new(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version);
_clientInfoService = new ClientInfoService(args.ClientInfo.Name, args.ClientInfo.Version, new ClientCapabilitiesService(args.Capabilities.IsStateful));
await _logger.LogDebugAsync($"Connection established with '{_client.Id}', protocol version {_client.Version}").ConfigureAwait(false);

INamedFeatureCapability? namedFeatureCapability = ServiceProvider.GetTestFrameworkCapabilities().GetCapability<INamedFeatureCapability>();
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
#nullable enable
const Microsoft.Testing.Platform.ServerMode.JsonRpcStrings.IsStateful = "isStateful" -> string!
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider, bool IsStateful) -> void
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider, out bool IsStateful) -> void
Comment thread
Evangelink marked this conversation as resolved.
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.get -> bool
Microsoft.Testing.Platform.ServerMode.ClientCapabilities.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.<Clone>$() -> Microsoft.Testing.Platform.Services.ClientCapabilitiesService!
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ClientCapabilitiesService(bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Deconstruct(out bool IsStateful) -> void
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? other) -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.get -> bool
Microsoft.Testing.Platform.Services.ClientCapabilitiesService.IsStateful.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Microsoft.Testing.Platform.Services.ClientInfoService.Capabilities.init -> void
Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version, Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version, out Microsoft.Testing.Platform.Services.IClientCapabilities! Capabilities) -> void
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.Equals(object? obj) -> bool
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.GetHashCode() -> int
override Microsoft.Testing.Platform.Services.ClientCapabilitiesService.ToString() -> string!
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator !=(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
static Microsoft.Testing.Platform.Services.ClientCapabilitiesService.operator ==(Microsoft.Testing.Platform.Services.ClientCapabilitiesService? left, Microsoft.Testing.Platform.Services.ClientCapabilitiesService? right) -> bool
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.ClientCapabilities(bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.ServerMode.ClientCapabilities.Deconstruct(out bool DebuggerProvider) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.ClientInfoService(string! Id, string! Version) -> void
*REMOVED*Microsoft.Testing.Platform.Services.ClientInfoService.Deconstruct(out string! Id, out string! Version) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.ReadFields(System.IO.Stream! stream, System.Func<ushort, int, bool>! tryReadField) -> void
static Microsoft.Testing.Platform.IPC.Serializers.BaseSerializer.WriteListPayload<T>(System.IO.Stream! stream, ushort fieldId, T[]? list, System.Action<System.IO.Stream!, T>! writeItem) -> void
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
#nullable enable
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities
[TPEXP]Microsoft.Testing.Platform.Services.IClientCapabilities.IsStateful.get -> bool
[TPEXP]Microsoft.Testing.Platform.Services.IClientInfo.Capabilities.get -> Microsoft.Testing.Platform.Services.IClientCapabilities!
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,8 +128,13 @@ private static void RegisterDefaultDeserializers(Dictionary<Type, JsonDeserializ
{
jsonElement.TryGetProperty(JsonRpcStrings.Testing, out JsonElement testing);

bool isStateful = testing.ValueKind == JsonValueKind.Object
&& testing.TryGetProperty(JsonRpcStrings.IsStateful, out JsonElement statefulElement)
&& statefulElement.ValueKind == JsonValueKind.True;
Comment thread
Evangelink marked this conversation as resolved.

return new ClientCapabilities(
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider));
DebuggerProvider: json.Bind<bool>(testing, JsonRpcStrings.DebuggerProvider),
IsStateful: isStateful);
Comment thread
Evangelink marked this conversation as resolved.
});

deserializers[typeof(InitializeResponseArgs)] = new JsonElementDeserializer<InitializeResponseArgs>(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ internal static class JsonRpcStrings
public const string Capabilities = "capabilities";
public const string Testing = "testing";
public const string DebuggerProvider = "debuggerProvider";
public const string IsStateful = "isStateful";
public const string SupportsDiscovery = "supportsDiscovery";
public const string MultiRequestSupport = "experimental_multiRequestSupport";
public const string VSTestProviderSupport = "vstestProvider";
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,7 +90,7 @@ internal sealed record InvalidRequestParamsArgs(int ErrorCode, string ErrorMessa

internal sealed record ClientInfo(string Name, string Version);

internal sealed record ClientCapabilities(bool DebuggerProvider);
internal sealed record ClientCapabilities(bool DebuggerProvider, bool IsStateful);
Comment thread
Evangelink marked this conversation as resolved.

internal sealed record ServerInfo(string Name, string Version);

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,8 +110,9 @@ private static void RegisterDeserializers()
IDictionary<string, object?> capabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(properties, JsonRpcStrings.Capabilities);
IDictionary<string, object?> testingCapabilities = GetRequiredPropertyFromJson<IDictionary<string, object?>>(capabilities, JsonRpcStrings.Testing);
bool debuggerProvider = GetRequiredPropertyFromJson<bool>(testingCapabilities, JsonRpcStrings.DebuggerProvider);
bool isStateful = GetOptionalPropertyFromJson(testingCapabilities, JsonRpcStrings.IsStateful) as bool? ?? false;

return new ClientCapabilities(debuggerProvider);
return new ClientCapabilities(debuggerProvider, isStateful);
});

Deserializers[typeof(InitializeResponseArgs)] = new ObjectDeserializer<InitializeResponseArgs>(properties =>
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientCapabilitiesService(bool IsStateful) : IClientCapabilities;
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,4 +3,4 @@

namespace Microsoft.Testing.Platform.Services;

internal sealed record ClientInfoService(string Id, string Version) : IClientInfo;
internal sealed record ClientInfoService(string Id, string Version, IClientCapabilities Capabilities) : IClientInfo;
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.Testing.Platform.Services;

/// <summary>
/// Represents the capabilities declared by the client that is driving the test host.
/// </summary>
/// <remarks>
/// Capabilities are opt-in: unless a client explicitly declares a capability, the platform assumes the
/// most conservative (default) behavior. This lets a test framework tailor its behavior to how the client
/// intends to consume the results without having to guess based on the environment or transport.
/// </remarks>
[Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")]
public interface IClientCapabilities
{
/// <summary>
/// Gets a value indicating whether the client is stateful.
/// </summary>
/// <remarks>
/// A stateful client persists an addressable set of test nodes for the whole session and keeps each node in
/// its last-known state until it is explicitly updated (for example, an IDE test explorer). A stateless client
/// consumes updates as a stream and does not retain node state after the run (for example, <c>dotnet test</c>).
/// The default is <see langword="false"/> (stateless); a client opts into stateful behavior.
/// </remarks>
bool IsStateful { get; }
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,4 +18,9 @@ public interface IClientInfo
/// Gets the client version.
/// </summary>
string Version { get; }

/// <summary>
/// Gets the capabilities declared by the client.
/// </summary>
IClientCapabilities Capabilities { get; }
Comment thread
Evangelink marked this conversation as resolved.
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,4 +11,7 @@ public sealed record ClientCapabilities(

public sealed record ClientTestingCapabilities(
[property: JsonProperty("debuggerProvider")]
bool DebuggerProvider);
bool DebuggerProvider,

[property: JsonProperty("isStateful")]
bool IsStateful = false);
Comment thread
Evangelink marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,7 +18,7 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
[TestClass]
public sealed class ObjectModelConvertersTests
{
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0");
private static readonly IClientInfo ClientInfo = new ClientInfoService(WellKnownClients.VisualStudio, "1.0.0", new ClientCapabilitiesService(IsStateful: false));

[TestMethod]
[DataRow(true)]
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ public void Patch_WhenNoRunSettingsProvided_CreateRunSettingsWithResultsDirector
{
_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object,
new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -41,7 +41,7 @@ public void Patch_WithRunSettingsProvidedButMissingResultsDirectory_AddsElement(

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");

XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectory",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand All@@ -62,7 +62,7 @@ public void Patch_WithRunSettingsContainingResultsDirectory_EntryIsNotOverridden
""";

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty), _commandLineOptions.Object);
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)), _commandLineOptions.Object);
Assert.AreEqual(
"/PlatformResultDirectoryFromFile",
runSettingsDocument.XPathSelectElement("RunSettings/RunConfiguration/ResultsDirectory")!.Value);
Expand DownExpand Up@@ -91,7 +91,7 @@ public void Patch_WhenRunSettingsExists_MergesParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(runSettings, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand All@@ -115,7 +115,7 @@ public void Patch_WhenRunSettingsDoesNotExist_AddParameters()
});

_configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns("/PlatformResultDirectory");
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty),
XDocument runSettingsDocument = RunSettingsPatcher.Patch(null, _configuration.Object, new ClientInfoService(string.Empty, string.Empty, new ClientCapabilitiesService(IsStateful: false)),
_commandLineOptions.Object);

XElement[] testRunParameters = [.. runSettingsDocument.XPathSelectElements("RunSettings/TestRunParameters/Parameter")];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -202,6 +202,91 @@ public void GetProperties_WhenPropertiesDelegateIsNotSet_ThrowsInvalidOperationE
Assert.Contains(nameof(TestJsonObjectSerializer), exception.Message);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithIsStatefulTrue_StjPath_SurfacesStatefulClient()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true, "isStateful": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsTrue(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_InitializeRequest_WithoutIsStateful_StjPath_DefaultsToStateless()
{
// Arrange
Json json = new();
const string initializeParams = """
{
"processId": 1,
"clientInfo": { "name": "client", "version": "1.0.0" },
"capabilities": { "testing": { "debuggerProvider": true } }
}
""";

// Act
InitializeRequestArgs args = json.Deserialize<InitializeRequestArgs>(initializeParams.AsMemory());

// Assert
Assert.IsFalse(args.Capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithIsStatefulTrue_JsonitePath_SurfacesStatefulClient()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
["isStateful"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsTrue(capabilities.IsStateful);
}

[TestMethod]
public void Deserialize_ClientCapabilities_WithoutIsStateful_JsonitePath_DefaultsToStateless()
{
// Arrange
Dictionary<string, object?> properties = new()
{
["capabilities"] = new Dictionary<string, object?>
{
["testing"] = new Dictionary<string, object?>
{
["debuggerProvider"] = true,
},
},
};

// Act
ClientCapabilities capabilities = SerializerUtilities.Deserialize<ClientCapabilities>(properties);

// Assert
Assert.IsFalse(capabilities.IsStateful);
}

private sealed class TestJsonObjectSerializer : JsonObjectSerializer;

private sealed class Person
Expand Down
Loading