Add [MemberCondition] attribute for static-member-based test conditions (#9070)#9071
Conversation
Adds a new `Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute` deriving from `ConditionBaseAttribute` that evaluates one or more static `bool` members (property, field, or parameterless method) referenced by `Type` + member name to decide whether a test class or test method runs. This mirrors the `[ConditionalFact]` / `[ConditionalTheory]` pattern from `Microsoft.DotNet.XUnitExtensions` used heavily by dotnet/runtime, dotnet/sdk, and dotnet/aspnetcore, removing the need to define a one-off `ConditionBaseAttribute` subclass per condition (e.g. `Is64BitProcessCondition`). Behavior: - Multiple member names within a single attribute are AND-combined. - Each attribute instance gets a unique `GroupName` derived from the type and members, so stacking multiple `[Condition]` attributes ANDs them across instances (matching xUnit's usage pattern). - `ConditionMode.Exclude` flips the semantics (skip when the condition holds). - Member resolution looks up public/non-public `static bool` property, field, or parameterless method (in that order). On any resolution failure (missing, non-bool, non-static, instance-only, or method with parameters), `IsConditionMet` throws `InvalidOperationException` so the test fails rather than silently skipping -- avoiding the classic "typo in member name silently disables the test" pitfall. Provides four CLS-friendly ctor overloads (`(Type, string)`, `(ConditionMode, Type, string)` plus `params`-array variants marked `[CLSCompliant(false)]`). Uses `DynamicallyAccessedMembers` annotations for trimming/AOT correctness. Targets all existing TFMs (netstandard2.0, net462, net8.0, net9.0). Adds 23 unit tests in `TestFramework.UnitTests` covering argument validation, member resolution variants (public/non-public, property/field/method), AND-semantics, `ConditionMode.Exclude`, error messages, and `GroupName` uniqueness. Fixes #9070. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new MSTest public attribute, Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute, enabling conditional execution of test methods/classes based on one or more referenced static bool members (property/field/parameterless method) specified by Type + member name, along with unit tests and public API declarations.
Changes:
- Introduces
ConditionAttributeimplementation (member resolution + AND semantics + grouping + include/exclude modes). - Adds unit tests validating constructor validation, resolution behavior, grouping, and failure modes.
- Updates
PublicAPI.Unshipped.txtto include the new public API surface.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs | Adds unit tests covering ConditionAttribute behavior and error cases. |
| src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt | Declares the new public API entries for ConditionAttribute. |
| src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs | Implements the new ConditionAttribute (reflection-based member evaluation, grouping, and messages). |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 2
Per design review, restrict the resolver to `BindingFlags.Public | Static` (was `Public | NonPublic | Static`). Rationale: - The `conditionType` is effectively acting as a public API surface for the condition. Non-public members of another type don't belong in that contract. - Slimmer `DynamicallyAccessedMembers` annotations for trim/AOT (3 flags instead of 6) -- only public properties, fields, and methods are preserved. - Matches xUnit's `MemberData` resolution which defaults to `Public | Static`. - Avoids surprises with `InternalsVisibleTo` when the condition type lives in a different assembly than the test. Error messages updated to say "public static bool"; XML docs aligned. Test `IsConditionMet_NonPublicStaticProperty_IsResolved` flipped to `IsConditionMet_NonPublicStaticProperty_ThrowsInvalidOperation`. All 23 tests still pass on net8.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Evangelink
left a comment
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
Expert Review — PR #9071 [Condition] attribute
Overall the implementation is well-structured, correctly annotated for AOT/trimming ([DynamicallyAccessedMembers]), follows the existing ConditionBaseAttribute contract, and has solid positive-path test coverage. Three actionable issues below; one is a design correctness concern that should be fixed before this ships as public API.
Verdict Table
| # | Dimension | Status | Severity |
|---|---|---|---|
| 1 | Algorithmic Correctness | MODERATE | |
| 2 | Threading & Concurrency | ✅ LGTM | — |
| 3 | Security & IPC Contract Safety | ✅ LGTM | — |
| 4 | Public API & Binary Compatibility | MODERATE | |
| 5 | Performance & Allocations | MODERATE | |
| 6 | Cross-TFM Compatibility | ✅ LGTM | — |
| 7 | Resource & IDisposable Management | N/A | — |
| 8 | Defensive Coding at Boundaries | ✅ LGTM | — |
| 9 | Localization & Resources | ✅ LGTM | — |
| 10 | Test Isolation | ✅ LGTM | — |
| 11 | Assertion Quality | ✅ LGTM | — |
| 12 | Flakiness Patterns | ✅ LGTM | — |
| 13 | Test Completeness & Coverage | MODERATE | |
| 14 | Data-Driven Test Coverage | ✅ LGTM | — |
| 15 | Code Structure & Simplification | ✅ LGTM | — |
| 16 | Naming & Conventions | ✅ LGTM | — |
| 17 | Documentation Accuracy | ✅ LGTM | — |
| 18 | Analyzer & Code Fix Quality | N/A | — |
| 19 | IPC Wire Compatibility | N/A | — |
| 20 | Build Infrastructure & Dependencies | ✅ LGTM | — |
| 21 | Scope & PR Discipline | ✅ LGTM | — |
| 22 | PowerShell Scripting Hygiene | N/A | — |
18/22 dimensions clean (4 N/A).
Issues (inline comments carry the detail)
MODERATE — GroupName omits ConditionMode (line 208 of ConditionAttribute.cs)
GroupName is keyed on {type}:{members} only. Two attributes with the same type+member names but opposite modes (Include and Exclude) collide into the same group and are OR-combined in AttributeExtensions.IsIgnored. The net result is the test always runs regardless of the condition value — a silent no-op. Because GroupName is now public API, the fix must land before this ships.
Fix: append :{Mode} to the format string.
MODERATE — EvaluateMember performs fresh reflection on every IsConditionMet access (line 233 of ConditionAttribute.cs)
For a class-level [Condition], IsConditionMet is called once per test method; each call performs up to three Get{Property,Field,Method} lookups on the live Type. Caching the resolved MemberInfo (or a compiled Func<bool>) at construction time would both improve performance and surface misconfigurations eagerly.
MODERATE — method.ReturnType != typeof(bool) branch has no test (line 260 of ConditionAttribute.cs)
The Conditions fixture includes a non-bool property (NotABool) and a method with a parameter (WithParam), but no static parameterless method returning a non-bool. The branch method.ReturnType != typeof(bool) in EvaluateMember is therefore unreachable in the current test suite.
NIT — IgnoreMessage_Exclude_HasExpectedText doesn't assert the type name (line 103 of test file)
The Include variant asserts .Contain(typeof(Conditions).FullName!) but the Exclude variant does not, leaving that half of the message-formatting code under-asserted.
🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review (on PR ready) workflow. · 879.9 AIC · ⌖ 14.1 AIC · ◷
This comment has been minimized.
This comment has been minimized.
…emberNames - Wrap `_conditionMemberNames` in a cached `ReadOnlyCollection<string>` so `ConditionMemberNames` can no longer be downcast to `string[]` and mutated. - In `EvaluateMember`, reject indexer properties (those with index parameters) alongside non-bool/non-readable properties so they consistently surface as `InvalidOperationException` instead of `TargetParameterCountException`. Use `GetGetMethod(nonPublic: true)` to reliably detect missing getters. - Add `ConditionMemberNames_CannotBeDowncastToMutableArray` test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…nd tests - `IsConditionMet` now resolves each member once and caches a `Func<bool>` delegate. Subsequent evaluations skip the per-call `GetProperty` / `GetField` / `GetMethod` lookups (~3xN reflection lookups per access). - `GroupName` now includes `Mode` so two `[Condition]` attributes with the same type/members but opposite modes no longer share a group (previously they would silently OR-combine and the test would always run). - `IsConditionMet` uses `All` for the AND short-circuit, addressing the CodeQL note. - Added `IsConditionMet_ParameterlessMethodWithNonBoolReturn_ThrowsInvalidOperation`, `GroupName_DifferentMode_AreDifferentGroups`, and tightened `IgnoreMessage_Exclude_HasExpectedText` to also assert the type FullName (matching the Include variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…ssary [CLSCompliant(false)] - The previous name was too generic and clashed conceptually with the existing ConditionBaseAttribute / OSConditionAttribute / CIConditionAttribute family. MemberConditionAttribute better reflects that the attribute targets a static member by name. - [CLSCompliant(false)] was not needed on the params string[] overloads: the existing DataRowAttribute(params object?[]?) and DynamicDataAttribute(string, params object?[]) constructors compile cleanly without it in the same CLS-compliant assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Per review feedback, pushed dd50f2b:
All 23 unit tests still pass on net8.0 (1252/1252 in |
🧪 Test quality grade — PR #907126 tests graded from 1 new file (
This advisory comment was generated automatically. Grades are heuristic
|
- Add `BindingFlags.FlattenHierarchy` to the member lookup so `[MemberCondition(typeof(Derived), nameof(Base.SomeFlag))]` resolves the inherited static member via the derived type, matching how C# allows accessing public static base members through a derived type name. - Tighten the property validation to require a public getter (`GetGetMethod(nonPublic: false)`). A public static property with a private getter doesn't satisfy the documented `public static bool readable property` contract and isn't guaranteed to be preserved by the `PublicProperties` trimming annotation on `ConditionType`. - Add `IsConditionMet_InheritedStaticProperty_ResolvesViaDerivedType` and `IsConditionMet_PropertyWithPrivateGetter_ThrowsInvalidOperation` tests (plus matching fixture types). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes #9070.
Summary
Adds a new public attribute
Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttributethat derives fromConditionBaseAttributeand lets users gate a test class or test method on one or morestatic boolmembers (property, field, or parameterless method) referenced byType+ member name — without having to write a dedicatedConditionBaseAttributesubclass per condition.This mirrors the well-known
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindows7))]pattern fromMicrosoft.DotNet.XUnitExtensionsused heavily bydotnet/runtime,dotnet/sdk, anddotnet/aspnetcore.The name
MemberConditionwas picked to align with the existingOSConditionAttribute/CIConditionAttributefamily and to clearly convey that the condition is resolved against a member identified by name (rather than a hard-coded predicate).Example
Behavior
IsConditionMet.GroupNamederived from the type and members, so stacking multiple[MemberCondition]attributes ANDs them across instances (this matches how the existingAttributeHelpers.IsIgnoredgroups conditions — OR within a group, AND across groups).ConditionMode.Excludeflips the semantics (skip when the condition holds), consistent withOSConditionAttribute/CIConditionAttribute.public static boolproperty, field, or parameterless method (in that order). Non-public members are intentionally excluded — keep your condition helperspublic, like the xUnit[ConditionalFact]pattern.IsConditionMetthrowsInvalidOperationExceptionso the test fails with anErroroutcome rather than silently skipping — avoiding the classic "typo in member name silently disables the test" pitfall.API surface
Four constructor overloads:
Uses
DynamicallyAccessedMembersannotations on theTypeparameter for trimming/AOT correctness (covers public properties, fields, and methods). Targets all existing TFMs (netstandard2.0,net462,net8.0,net9.0).PublicAPI.Unshipped.txtupdated accordingly.Tests
Adds 23 unit tests in
TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cscovering:InvalidOperationException)ConditionMode.ExcludeGroupNameuniqueness for distinct attribute instancesAll 23 tests pass on
net8.0(1252/1252 inTestFramework.UnitTests).MSTestAdapter.PlatformServicesstill builds clean (no churn needed — the existingAttributeHelpers.IsIgnoredalready handles arbitraryConditionBaseAttributesubclasses).Follow-ups
IsConditionMeton resolution failure are currently inline strings. A small follow-up could move them toFrameworkMessages.resxfor localization; doing it here would have inflated the diff with the 14-language.xlfregeneration.static boolproperty/field/parameterless method (similar to MSTest's existingDataRow/DynamicDataanalyzers) would catch typos at build time — worth a separate issue.