Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.ObjectModel;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Conditionally runs or ignores a test class or test method based on the value of one or more
/// <see langword="static"/> <see cref="bool"/> members (property, field, or parameterless method)
/// referenced by <see cref="Type"/> and member name.
/// </summary>
/// <remarks>
/// <para>
/// When multiple member names are supplied to a single attribute, their values are combined with
/// a logical AND: the attribute's <see cref="IsConditionMet"/> is <see langword="true"/> only if
/// every referenced member evaluates to <see langword="true"/>.
/// </para>
/// <para>
/// Each <see cref="ConditionAttribute"/> instance forms its own <see cref="ConditionBaseAttribute.GroupName"/>,
/// so stacking multiple <see cref="ConditionAttribute"/> declarations on the same target is combined
/// with a logical AND, matching the typical <c>[ConditionalFact]</c> usage pattern in other test frameworks.
/// </para>
/// <para>
/// If the referenced member cannot be found as a <see langword="public"/> <see langword="static"/>
/// <see cref="bool"/> property, field, or parameterless method, or (for methods) requires parameters,
/// evaluating <see cref="IsConditionMet"/> throws an <see cref="InvalidOperationException"/>. This
/// surfaces as a test error rather than a silent skip so typos and refactors don't accidentally
/// disable tests.
/// </para>
/// <para>
/// This attribute isn't inherited. Applying it to a base class will not affect derived classes.
/// </para>
/// <example>
/// <code>
/// [TestMethod]
/// [Condition(typeof(Environment), nameof(Environment.Is64BitProcess))]
/// public void Only_Runs_On_64Bit() { }
///
/// [TestMethod]
/// [Condition(typeof(PlatformDetection),
/// nameof(PlatformDetection.IsNotBrowser),
/// nameof(PlatformDetection.IsThreadingSupported))]
/// public void Requires_Threading_And_Not_Browser() { }
///
/// [TestMethod]
/// [Condition(ConditionMode.Exclude, typeof(PlatformDetection), nameof(PlatformDetection.IsMonoRuntime))]
/// public void Does_Not_Run_On_Mono() { }
/// </code>
/// </example>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class ConditionAttribute : ConditionBaseAttribute
Comment thread
Evangelink marked this conversation as resolved.
Outdated
{
private const DynamicallyAccessedMemberTypes RequiredMembers =
DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.PublicMethods;

private readonly string[] _conditionMemberNames;
private string? _groupName;
private ReadOnlyCollection<string>? _conditionMemberNamesView;

/// <summary>
/// Initializes a new instance of the <see cref="ConditionAttribute"/> class with
/// <see cref="ConditionMode.Include"/> semantics: the test runs only when the referenced
/// member evaluates to <see langword="true"/>.
/// </summary>
/// <param name="conditionType">The type declaring the static member to evaluate.</param>
/// <param name="conditionMemberName">
/// The name of the <see langword="public"/> <see langword="static"/> <see cref="bool"/> member
/// (property, field, or parameterless method) to evaluate.
/// </param>
public ConditionAttribute(
[DynamicallyAccessedMembers(RequiredMembers)] Type conditionType,
string conditionMemberName)
: this(ConditionMode.Include, conditionType, conditionMemberName, additionalConditionMemberNames: [])
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ConditionAttribute"/> class with
/// <see cref="ConditionMode.Include"/> semantics: the test runs only when every referenced
/// member evaluates to <see langword="true"/>.
/// </summary>
/// <param name="conditionType">The type declaring the static member(s) to evaluate.</param>
/// <param name="conditionMemberName">
/// The name of the first <see langword="public"/> <see langword="static"/> <see cref="bool"/>
/// member (property, field, or parameterless method) to evaluate.
/// </param>
/// <param name="additionalConditionMemberNames">
/// Additional <see langword="public"/> <see langword="static"/> <see cref="bool"/> member
/// name(s) to evaluate. All referenced members are AND-combined.
/// </param>
[CLSCompliant(false)]
public ConditionAttribute(
[DynamicallyAccessedMembers(RequiredMembers)] Type conditionType,
string conditionMemberName,
params string[] additionalConditionMemberNames)
: this(ConditionMode.Include, conditionType, conditionMemberName, additionalConditionMemberNames)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ConditionAttribute"/> class.
/// </summary>
/// <param name="mode">
/// Whether the test should be included (run when the condition is met) or excluded
/// (skipped when the condition is met).
/// </param>
/// <param name="conditionType">The type declaring the static member to evaluate.</param>
/// <param name="conditionMemberName">
/// The name of the <see langword="public"/> <see langword="static"/> <see cref="bool"/> member
/// (property, field, or parameterless method) to evaluate.
/// </param>
public ConditionAttribute(
ConditionMode mode,
[DynamicallyAccessedMembers(RequiredMembers)] Type conditionType,
string conditionMemberName)
: this(mode, conditionType, conditionMemberName, additionalConditionMemberNames: [])
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ConditionAttribute"/> class.
/// </summary>
/// <param name="mode">
/// Whether the test should be included (run when the condition is met) or excluded
/// (skipped when the condition is met).
/// </param>
/// <param name="conditionType">The type declaring the static member(s) to evaluate.</param>
/// <param name="conditionMemberName">
/// The name of the first <see langword="public"/> <see langword="static"/> <see cref="bool"/>
/// member (property, field, or parameterless method) to evaluate.
/// </param>
/// <param name="additionalConditionMemberNames">
/// Additional <see langword="public"/> <see langword="static"/> <see cref="bool"/> member
/// name(s) to evaluate. All referenced members are AND-combined.
/// </param>
[CLSCompliant(false)]
public ConditionAttribute(
ConditionMode mode,
[DynamicallyAccessedMembers(RequiredMembers)] Type conditionType,
string conditionMemberName,
params string[] additionalConditionMemberNames)
: base(mode)
{
ConditionType = conditionType ?? throw new ArgumentNullException(nameof(conditionType));
if (conditionMemberName is null)
{
throw new ArgumentNullException(nameof(conditionMemberName));
}

if (StringEx.IsNullOrWhiteSpace(conditionMemberName))
{
throw new ArgumentException(
"Condition member name must not be empty or whitespace.",
nameof(conditionMemberName));
}

if (additionalConditionMemberNames is null || additionalConditionMemberNames.Length == 0)
{
_conditionMemberNames = [conditionMemberName];
}
else
{
_conditionMemberNames = new string[additionalConditionMemberNames.Length + 1];
_conditionMemberNames[0] = conditionMemberName;
for (int i = 0; i < additionalConditionMemberNames.Length; i++)
{
string name = additionalConditionMemberNames[i];
if (StringEx.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"Condition member names must not be null, empty, or whitespace.",
nameof(additionalConditionMemberNames));
}

_conditionMemberNames[i + 1] = name;
}
}

IgnoreMessage = mode == ConditionMode.Include
? $"Test is only supported when ({FormatMemberList()}) on '{conditionType.FullName ?? conditionType.Name}' is true."
: $"Test is not supported when ({FormatMemberList()}) on '{conditionType.FullName ?? conditionType.Name}' is true.";
}

/// <summary>
/// Gets the type declaring the <see langword="static"/> member(s) used to evaluate the condition.
/// </summary>
[DynamicallyAccessedMembers(RequiredMembers)]
public Type ConditionType { get; }

/// <summary>
/// Gets the name(s) of the <see langword="static"/> <see cref="bool"/> member(s) (property,
/// field, or parameterless method) on <see cref="ConditionType"/> evaluated for this condition.
/// Multiple values are combined with a logical AND.
/// </summary>
public IReadOnlyList<string> ConditionMemberNames
=> _conditionMemberNamesView ??= new ReadOnlyCollection<string>(_conditionMemberNames);

/// <inheritdoc />
/// <remarks>
/// Each <see cref="ConditionAttribute"/> instance produces a group name derived from
/// <see cref="ConditionType"/> and <see cref="ConditionMemberNames"/>, so stacking multiple
/// <see cref="ConditionAttribute"/> declarations on the same target combines them with a
/// logical AND.
/// </remarks>
public override string GroupName
=> _groupName ??= $"{nameof(ConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}";
Comment thread
Evangelink marked this conversation as resolved.
Outdated

/// <inheritdoc />
/// <remarks>
/// All referenced members are evaluated in order and combined with a logical AND. Throws
/// <see cref="InvalidOperationException"/> if a member can't be resolved as a
/// <see langword="public"/> <see langword="static"/> <see cref="bool"/> property, field, or
/// parameterless method.
/// </remarks>
public override bool IsConditionMet
{
get
{
foreach (string memberName in _conditionMemberNames)
{
if (!EvaluateMember(memberName))
{
return false;
}
}
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

return true;
}
}

private bool EvaluateMember(string memberName)
Comment thread
Evangelink marked this conversation as resolved.
Outdated
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static;
string typeName = ConditionType.FullName ?? ConditionType.Name;

PropertyInfo? property = ConditionType.GetProperty(memberName, Flags);
if (property is not null)
{
return property.PropertyType != typeof(bool)
|| property.GetIndexParameters().Length != 0
|| property.GetGetMethod(nonPublic: true) is null
? throw new InvalidOperationException(
$"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [Condition].")
: (bool)property.GetValue(null)!;
}

FieldInfo? field = ConditionType.GetField(memberName, Flags);
if (field is not null)
{
return field.FieldType != typeof(bool)
? throw new InvalidOperationException(
$"Member '{typeName}.{memberName}' must be a public static bool field to be used with [Condition].")
: (bool)field.GetValue(null)!;
}

MethodInfo? method = ConditionType.GetMethod(memberName, Flags, binder: null, types: Type.EmptyTypes, modifiers: null);
return method is null
? throw new InvalidOperationException(
$"Could not find a public static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'.")
: method.ReturnType != typeof(bool)
Comment thread
Evangelink marked this conversation as resolved.
Outdated
? throw new InvalidOperationException(
$"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [Condition].")
: (bool)method.Invoke(null, null)!;
}

private string FormatMemberList()
=> _conditionMemberNames.Length == 1
? _conditionMemberNames[0]
: string.Join(" AND ", _conditionMemberNames);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.As
Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.FixtureType.get -> System.Type!
Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ActualText.get -> string?
Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ExpectedText.get -> string?
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionMemberNames.get -> System.Collections.Generic.IReadOnlyList<string!>!
Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionType.get -> System.Type!
override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.GroupName.get -> string!
override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.IsConditionMet.get -> bool
Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder
Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder = 1 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder
Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InOrder = 0 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder
Expand Down
Loading
Loading