Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ When making change to resource files, you MUST:
- Every API marked with `[Experimental]` MUST include this sentence in its XML documentation `<remarks>`: `This API is experimental. It may change, break, or be removed at any time without notice.` Documentation tooling does not reliably surface the attribute itself.
- Add the sentence in a `<para>` when `<remarks>` already contains other text; otherwise, add a new `<remarks>` block.
- Apply this rule to experimental members as well as types.
- When designing a capability API or protocol field, consider clients that predate the capability. Model "unsupported or not declared" separately from an explicit value (for example, with a nullable value or presence-aware representation) unless absence is intentionally equivalent to the default, and document the compatibility behavior.

## Testing Guidelines

Expand Down
9 changes: 5 additions & 4 deletions docs/mstest-runner-protocol/001-protocol-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,11 @@ interface InitializeParams {

// 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`). This is independent of connection lifetime and
// experimental_multiRequestSupport. Defaults to false.
// (for example, an IDE test explorer). If false, the client explicitly declares that it is
// stateless and consumes test updates as a stream without retaining node state after the run
// (for example, `dotnet test`). If missing, the client does not support or did not declare
// the capability, allowing consumers to apply compatibility behavior for known legacy clients.
// This is independent of connection lifetime and experimental_multiRequestSupport.
isStateful?: boolean,
},
}
Expand Down
3 changes: 1 addition & 2 deletions docs/mstest-runner-protocol/server-mode-1.0.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@
"type": "boolean"
},
"isStateful": {
"type": "boolean",
"default": false
"type": "boolean"
}
},
"additionalProperties": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ internal static string ReadRunSettings(string[]? fileNames, IFileSystem fileSyst
private static XDocument Patch(string? runSettingsXml, IConfiguration configuration, IClientInfo client, ICommandLineOptions commandLineOptions)
{
// Keep recognizing older Visual Studio clients that predate the statefulness capability.
bool isDesignMode = client.Capabilities.IsStateful || client.Id == WellKnownClients.VisualStudio;
bool isDesignMode = client.Capabilities.GetIsStateful()
?? client.Id == WellKnownClients.VisualStudio;
XDocument runSettingsDocument = PatchSettingsWithDefaults(runSettingsXml, isDesignMode, configuration);
PatchTestRunParameters(runSettingsDocument, commandLineOptions);
return runSettingsDocument;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private static bool IsAlwaysTrue(IInvocationOperation operation)
"IsTrue" => AssertConditionAnalyzerHelper.GetConditionArgument(operation) is { ConstantValue: { HasValue: true, Value: true } },
"IsFalse" => AssertConditionAnalyzerHelper.GetConditionArgument(operation) is { ConstantValue: { HasValue: true, Value: false } },
"AreEqual" => !AssertConditionAnalyzerHelper.HasNonDefaultEqualityComparerArgument(operation)
&& !IsEnumUnderlyingValueContractAssertion(operation)
&& (AssertConditionAnalyzerHelper.GetEqualityStatus(operation, AssertConditionAnalyzerHelper.ExpectedParameterName) == AssertConditionAnalyzerHelper.EqualityStatus.Equal
|| AssertConditionAnalyzerHelper.HasIdenticalExpectedAndActualWithBuiltInEquality(operation, AssertConditionAnalyzerHelper.ExpectedParameterName)),
"AreNotEqual" => !AssertConditionAnalyzerHelper.HasNonDefaultEqualityComparerArgument(operation)
Expand All @@ -78,4 +79,64 @@ private static bool IsAlwaysTrue(IInvocationOperation operation)
"IsNotNull" => AssertConditionAnalyzerHelper.GetValueArgument(operation) is { } valueArgumentOperation && AssertConditionAnalyzerHelper.IsNotNullableType(valueArgumentOperation),
_ => false,
};

private static bool IsEnumUnderlyingValueContractAssertion(IInvocationOperation operation)
{
IOperation? expectedArgument = operation.Arguments.FirstOrDefault(argument => argument.Parameter?.Name == AssertConditionAnalyzerHelper.ExpectedParameterName)?.Value;
IOperation? actualArgument = operation.Arguments.FirstOrDefault(argument => argument.Parameter?.Name == AssertConditionAnalyzerHelper.ActualParameterName)?.Value;

return expectedArgument is not null
&& actualArgument is not null
&& ((IsNumericLiteral(expectedArgument) && IsEnumMemberConvertedToUnderlyingType(actualArgument))
|| (IsEnumMemberConvertedToUnderlyingType(expectedArgument) && IsNumericLiteral(actualArgument)));
}

private static bool IsNumericLiteral(IOperation operation)
=> WalkDownImplicitConversionsAndParentheses(operation) switch
{
ILiteralOperation { Type.SpecialType: var specialType } => IsIntegralNumericType(specialType),
IUnaryOperation { OperatorKind: UnaryOperatorKind.Plus or UnaryOperatorKind.Minus, Operand: { } operand } => IsNumericLiteral(operand),
_ => false,
};

private static bool IsIntegralNumericType(SpecialType specialType)
=> specialType is SpecialType.System_SByte
or SpecialType.System_Byte
or SpecialType.System_Int16
or SpecialType.System_UInt16
or SpecialType.System_Int32
or SpecialType.System_UInt32
or SpecialType.System_Int64
or SpecialType.System_UInt64;

private static bool IsEnumMemberConvertedToUnderlyingType(IOperation operation)
{
operation = WalkDownImplicitConversionsAndParentheses(operation);
if (operation is not IConversionOperation
{
IsImplicit: false,
Type: { } convertedType,
Operand: { } operand,
})
{
return false;
}

operand = WalkDownImplicitConversionsAndParentheses(operand);
return GetEnumUnderlyingType(operand) is { } underlyingType
&& SymbolEqualityComparer.Default.Equals(convertedType, underlyingType);
}

private static ITypeSymbol? GetEnumUnderlyingType(IOperation operation)
=> operation is IFieldReferenceOperation { Field: { HasConstantValue: true, ContainingType: { TypeKind: TypeKind.Enum } enumType } }
? enumType.EnumUnderlyingType
: null;

private static IOperation WalkDownImplicitConversionsAndParentheses(IOperation operation)
=> operation switch
{
IConversionOperation { IsImplicit: true } conversion => WalkDownImplicitConversionsAndParentheses(conversion.Operand),
IParenthesizedOperation parenthesizedOperation => WalkDownImplicitConversionsAndParentheses(parenthesizedOperation.Operand),
_ => operation,
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers;
Expand Down Expand Up @@ -58,6 +58,7 @@ public static string EmitSupportTypes()
using (sb.Block("internal sealed class TestMethodReflectionInfo"))
{
sb.AppendLine("public string Name { get; set; } = string.Empty;");
sb.AppendLine("public Type DeclaringType { get; set; } = null!;");
sb.AppendLine("public bool IsTestMethod { get; set; }");
sb.AppendLine("public bool IsStatic { get; set; }");
sb.AppendLine("public bool IsAsync { get; set; }");
Expand Down Expand Up @@ -243,6 +244,7 @@ private static void EmitMethods(IndentedStringBuilder sb, string fqn, TestClassM
using (sb.Block(null))
{
sb.AppendLine($"Name = \"{Escape(method.Name)}\",");
sb.AppendLine($"DeclaringType = typeof({method.DeclaringTypeFullyQualifiedName}),");
sb.AppendLine($"IsTestMethod = {Bool(method.IsTestMethod)},");
sb.AppendLine($"IsStatic = {Bool(method.IsStatic)},");
sb.AppendLine($"IsAsync = {Bool(method.IsAsync)},");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList<T
// reflection for that one method) rather than throwing out of the [ModuleInitializer],
// which would fault registration for the whole assembly.
sb.AppendLine("availableMethods ??= type.GetMethods(memberFlags);");
sb.AppendLine("MethodInfo? methodInfo = ResolveMethod(availableMethods, method.Name, method.ParameterTypes);");
sb.AppendLine("MethodInfo? methodInfo = ResolveMethod(availableMethods, method.DeclaringType, method.Name, method.ParameterTypes);");
using (sb.Block("if (methodInfo is not null)"))
{
sb.AppendLine("methodInvokers[methodInfo] = method.Invoke;");
Expand Down Expand Up @@ -280,12 +280,12 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList<T

private static void EmitResolveMethodHelper(IndentedStringBuilder sb)
{
sb.AppendLine("private static MethodInfo? ResolveMethod(MethodInfo[] availableMethods, string name, Type[] parameterTypes)");
sb.AppendLine("private static MethodInfo? ResolveMethod(MethodInfo[] availableMethods, Type declaringType, string name, Type[] parameterTypes)");
using (sb.Block(null))
{
using (sb.Block("foreach (MethodInfo candidate in availableMethods)"))
{
using (sb.Block("if (candidate.Name != name)"))
using (sb.Block("if (candidate.DeclaringType != declaringType || candidate.Name != name)"))
{
sb.AppendLine("continue;");
}
Expand Down
Loading