Design explicit tests for MSTest - #10606
Conversation
Define the API, selection model, data-row behavior, host integration, diagnostics, compatibility, and test plan for microsoft#5346. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 22c4cda6-f4c9-4492-89a0-9e2d80516d43 🤖
There was a problem hiding this comment.
Pull request overview
Defines RFC 024 for opt-in MSTest explicit-test behavior across VSTest and native MTP.
Changes:
- Specifies public APIs, activation rules, lifecycle, diagnostics, retries, and compatibility.
- Defines implementation surfaces and testing plans for both hosts.
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The direct-selection table classified every node matched by --treenode-filter or a server graph filter as activated. That grammar has negation, != property predicates, and wildcards, so an exclusion-only or match-all filter would have activated explicit tests and /** would have been Run All that runs them. Give the tree grammar its own (matches, activates) algebra, require a discriminating non-root segment to activate, and state the fail-closed rule that activation is never assumed when it cannot be determined. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cut it from 1081 to 604 lines. Every rule is still there, what went away is the repository history recital, the per-file implementation table, and the numbered acceptance test lists. Motivation now opens with three tests you would actually mark explicit, and there is a section showing how you run them, from Test Explorer, from dotnet test, and from an opt-in CI job. Added a prior art table for NUnit, xUnit v3 and TUnit, and why we follow NUnit rather than TUnit. 🤖
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
docs/RFCs/024-Explicit-Tests.md:143
TestDataRow<T>is a row and does not implementITestDataSource, so making it implement the source-named capability creates a misleading public type relationship. The existing ignore design keepsITestDataSourceIgnoreCapabilityon actual sources (DataRowAttribute/DynamicDataAttribute) while exposingIgnoreMessagedirectly onTestDataRow<T>and internalITestDataRow. Keep the explicit source capability and row properties separate in the same way.
`DataRowAttribute`, `DynamicDataAttribute`, and `TestDataRow<T>` implement the capability.
docs/RFCs/024-Explicit-Tests.md:435
- This no-user-code guarantee conflicts with the stated precedence that the assembly
ITestFilterruns first. The current path loads the test assembly, instantiates the registered filter, and callsFilterbefore resolving the test method, so module initializers, the filter constructor, andFilteritself can execute user code. Either move the explicit gate ahead of that policy or narrow this guarantee so protected tests do not rely on it.
The explicit check happens in `UnitTestRunner.RunSingleTestAsync`, before the test type is loaded and
therefore before assembly initialization, class initialization, construction, `TestInitialize`, and
the body. An assembly whose only selected tests are unactivated class, method, or unfolded-row
explicit tests runs no user code at all. Folded row declarations are the exception, see below.
TestDataRow<T> is a row, not an ITestDataSource, so making it implement ITestDataSourceExplicitCapability claimed a type relationship that does not exist. It now declares IsExplicit and ExplicitReason directly, next to IgnoreMessage, and the internal ITestDataRow exposes them, which is exactly how the ignore metadata is already split between sources and rows. Also narrow the gate guarantee. The explicit check runs after the assembly ITestFilter, and filter discovery loads the test assembly and then constructs and calls a registered [TestFilterProvider], so "runs no user code at all" was wrong. The guarantee is now no type load, no fixtures, no constructor, no TestInitialize and no body, the filter keeps its place ahead of the gate because it decides whether the test is in the run at all, and an ordering test pins the boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Both suppressed comments in the last review were right, fixed in 0b44dbd. On On the no user code guarantee: right as well, and it was wrong in two ways rather than one. I narrowed the claim rather than moving the gate. The filter answers whether the test belongs in the run at all, which is not the same question as whether the user asked for it, and a test the policy drops has to report as dropped rather than as an explicit skip, so putting the gate first would reverse that. The RFC now states the guarantee as no type load, no assembly or class initialization, no constructor, no 🤖 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (4)
docs/RFCs/024-Explicit-Tests.md:156
ITestDataSource.GetDatareturnsIEnumerable<object?[]>, so a custom source cannot returnTestDataRow<T>values directly; it must wrap each row in a single-element array. DirectIEnumerable<TestDataRow<T>>works here only becauseDynamicDataadapts arbitrary enumerables. Clarify this distinction so implementers do not copy an invalid custom-source contract.
A custom `ITestDataSource` implements the capability to mark every row it produces, or returns
`TestDataRow<T>` to mark single rows:
docs/RFCs/024-Explicit-Tests.md:356
- This tree-filter example does not match MSTest categories.
MSTestTestNodeConverter.cs:175-185encodes categoryHardwareas metadata keyHardwarewith an empty value, whileTreeNodeFilter.Matching.cs:170-173interprets the bracket sides as metadata key/value, so[Category=Hardware]matches nothing. Use an actual key/value trait example, or include a compatible category-metadata change and define how key-only categories interact with the rule that[Name=*]is non-discriminating.
| `/*/*/*/*[Category=Hardware]` | yes |
docs/RFCs/024-Explicit-Tests.md:536
- The JSON schema currently sets
additionalProperties: falseformstest.execution(docs/testconfig.schema.json:89-93), so the documentedexplicitTestModeconfiguration will be reported as invalid unless that schema is updated. Add the schema surface to the implementation plan so IDE validation and completion ship with the parser change.
| Settings | three `ExplicitTestMode` values with existing precedence, plus localized resources |
docs/RFCs/024-Explicit-Tests.md:476
- Source-wide explicit metadata is available on the
ITestDataSourceExplicitCapabilityinstance beforeGetDataruns, just as source-wide ignore is checked before enumeration inTestMethodRunner.DataRow.cs:30-39. Deferring this gate until enumeration can execute a side-effecting data source even though it was not activated; the “metadata does not exist until the source runs” rationale applies only to row-level declarations. Gate source explicitness beforeGetDataand reserve the enumeration exception for row metadata.
Folded rows have no discovery identity, so class and method explicitness is checked first, and source
and row declarations are checked as the data is enumerated, before per-row `TestInitialize`, before
test-class construction where construction is per row, and before the body. Each unactivated row
The fail-closed section said an unsupported filter constrains the run and activates nothing, while the next paragraph said an unparseable one fails. A filter whose semantics are unknown cannot constrain anything either, so split the two questions: an unevaluable constraint keeps its existing failure, and an evaluable one whose activation cannot be classified activates nothing. Also: - Say how *[Explicit=True] reaches TreeNodeFilter. It matches [Key=Value] only against TestMetadataProperty, so Explicit is one, and pre-node filtering in MtpTestElementFilter reads it from the same source. - Scope ignore precedence to metadata that is actually reached, and document the folded parent that is explicit and unactivated, whose data source never runs. - Stop calling ExplicitTestMode=Run and --filter "Explicit=True" equivalent. Run widens activation, the filter narrows selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Moving the Explicit=True paragraphs put two paragraphs between "That gap" and the gap it referred to. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ion check The folded data path claimed explicit metadata does not exist until the source runs. That is true of rows only. Source-wide explicitness is a property on the ITestDataSource attribute instance, so it is readable without enumerating, the way the source-wide IgnoreMessage already is. An unactivated source-wide declaration now gates before GetData and produces one folded-parent result. Compatibility described version skew as if a mixed pair kept running. The adapter's module initializer compares the adapter and framework informational versions and throws on any mismatch, so a mixed pair fails first. The real boundary is an adapter older than 3.10.0, which predates that check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Both suppressed comments were right, fixed in 48a3ac1. The first one caught me asserting a guarantee the design could not deliver, which is worse than the hole it was meant to close. The corruption guarantee was unsupported. Right. With one field carrying the provenance, a reconstructed folded parent saying The defect was that a single field has nothing to be checked against, which is the same mistake as the boolean one round earlier, one size up. Split into The parent reason rule was incomplete. Also right, and the gap was wider than the "different reasons" case I had written: one source with a reason beside a sibling with none, and a source-specific reason beside a broader method or class one, both ended up attributing one source's explanation to a result standing for all of them. Taking your definition. A parent gated by a class or method declaration takes that declaration's reason, since one declaration covers the whole method and nothing can disagree with it. A parent gated by its sources takes the reason they agree on and reports none on any disagreement, reason versus no reason included. Per-source reasons are still reported where they belong, when the sources are resolved individually. Twelve rounds. I have said twice that I was stopping and pushed anyway, both times because the finding was in the same place: a persisted value that could let Run All execute an opt-in test. That is the one thing this design exists to prevent, so it has not felt like somewhere to stop on principle. It is worth saying plainly that three consecutive rounds found a defect in successive versions of the same fallback, each one my own, which is a fair signal that this corner deserves a human's eye more than another iteration of mine. Everything is answered, 🤖 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
docs/RFCs/024-Explicit-Tests.md:589
- This aggregate rule conflicts with the API-level precedence at lines 200–205 and the resolved answer at line 928. When a folded parent is explicit at method scope and all sources are also explicit with a shared reason, those sections say the more-specific source reason wins, while this paragraph says the method reason wins. Define the folded aggregate exception explicitly (including mixed method/source reasons) and add a corresponding test vector so reporting is deterministic.
A gated parent reports one result standing for everything under it, so its reason has to be true of
everything under it. A parent gated by a class or method declaration takes that declaration's reason,
the more specific of the two, because one declaration covers the whole method and there is nothing to
disagree with it. A parent gated by its sources takes the reason its sources agree on, and reports
none on any disagreement, including one source carrying a reason where a sibling carries none. Picking
the most specific available reason there would attribute one source's explanation to a result standing
for all of them. Each source's own reason is still reported when the sources are resolved
individually, which is where a per-source explanation belongs.
…eason rules The corruption claim implied a defence against a persisted value rewritten into a different valid value. Nothing gives that: a third provenance field is rewritten the same way, and Explicit=True rewritten to False defeats the feature before any of them are read. The rule covers metadata that cannot be believed, absent, malformed, unrecognized, or in an impossible combination, and the test plan no longer implies more. The folded aggregate reason also contradicted the per-test precedence chain and the resolved answer. Named it as the exception it is, in all three places, and added the vector where they diverge. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
docs/RFCs/024-Explicit-Tests.md:256
- These commands pass MTP options to
dotnet runitself, which does not recognize them. Add the--application-argument separator so the test executable receives--filter-uidand--treenode-filter.
dotnet run --filter-uid <uid>
dotnet run --treenode-filter "/*/*/DeviceTests/*"
…gory lives
The converter writes [TestCategory("Hardware")] as
TestMetadataProperty("Hardware", string.Empty), so a category is selected as
[Hardware=*]. Keying discrimination on the value alone classified that as
non-discriminating, which meant naming a category could never start its
explicit tests, and the opt-in suite is most of what this design is for. The
table also showed [Category=Hardware], which matches no MSTest node at all.
A = predicate now discriminates on a literal on either side. Any != predicate
still does not, whatever literals it carries, and neither does [Explicit=*],
because Explicit is written on every node so asking which nodes carry it
selects all of them. That is the only key written on every node and it is
reserved, so no user property lands in the same position.
The truth table tests run against nodes the converter produced rather than
hand-built property bags, which is what would have caught this.
Also added the -- separator to the dotnet run examples, which otherwise pass
the options to dotnet run rather than to the test executable.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
docs/RFCs/024-Explicit-Tests.md:793
- This discovery plan does not populate source metadata for the folded paths it is meant to protect.
AssemblyEnumerator.TryUnfoldITestDataSourcescurrently returns for either folding mode before it even reads the source attributes (AssemblyEnumerator.cs:216-231), and can return after the first source fails to unfold without inspecting later sources. If source declarations are merged only “while unfolding,” an all-explicit folded parent is neither gated nor selectable viaExplicit=True. Pre-scan all source declarations before any folding/expansion early return; enumerate rows only when unfolding proceeds.
| Discovery | `TypeEnumerator` reads class and method declarations, `AssemblyEnumerator` merges source and row declarations while unfolding |
…pelled The exact carve-out for [Explicit=*] was bypassed by a wildcarded key. TreeNodeFilter expands * inside a property name and matches the result against every metadata key, so [Exp*=*] matches the universal Explicit property on every node while carrying literal characters, which made Run All activate every explicit test. A predicate no node can fail names nothing, whatever spelling reaches it, so [Explicit=*], [Exp*=*] and [*=*] all reduce to "has the property every node has" and none activates. Pinning the value to one of the two, as [Explicit=True] or [Exp*=True] does, excludes half the tree and discriminates. [Hardware=*] still discriminates because its key matches a property most nodes do not carry. Discovery also had to move. TryUnfoldITestDataSources returns for both fold modes before it reads the attributes, and can return again once a source fails to unfold, so merging source declarations only while unfolding left an all-explicit folded parent neither gated nor selectable. The scan happens ahead of every early return. Test plan follows, with wildcard-key vectors and a discovery test for the fold strategies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
docs/RFCs/024-Explicit-Tests.md:767
- The reservation does not cover
[TestCategory], even though native MTP converts each category intoTestMetadataProperty(category, string.Empty)(MSTestTestNodeConverter.cs:175-185). Consequently,[TestCategory("ExplicitReason")]makes/**[ExplicitReason=*]filterable on MTP while VSTest does not resolve that category by the property name, contradicting the cross-host and reason-not-filterable guarantees.[TestCategory("Explicit")]also collides with the universal built-in key, so its normal[Explicit=*]tree filter now selects every node instead of that category. Define collision handling for category names too and include both cases in compatibility notes and cross-host vectors.
`Explicit` and `ExplicitReason` are reserved property names on both hosts, compared ordinal
case-insensitively, because both hosts match case-insensitively: `ValueExpression` builds its regex
with `RegexOptions.IgnoreCase`, and `TestMethodFilter`'s supported-property dictionary and its trait
fallback both use `OrdinalIgnoreCase`. Reserving only the exact spellings would leave
`[TestProperty("explicit", "True")]` colliding. A `[TestProperty]` whose name matches either reserved
name in any casing is not written to the metadata surface or to the traits, and discovery reports a
warning naming the test, so the built-in value is the only one either host can match.
|
|
||
| ## Summary | ||
|
|
||
| Add `[Explicit]` for test classes and methods, and `IsExplicit` for data sources and data rows. An |
There was a problem hiding this comment.
Could we rename this concept to OnDemand throughout ([OnDemand], IsOnDemand, OnDemandReason, OnDemandTestMode)? Explicit is framework jargon, whereas OnDemand communicates the behavior directly: the test remains discoverable but runs only when positively selected or when the run mode requests it. It also avoids Manual, which would imply the test cannot be automated even though CI filters can activate it.
There was a problem hiding this comment.
Not renamed, and recorded as an open question rather than answered quietly. There is a new Naming section putting both names side by side, and it is now one of the two rows in ## Open questions and named in the approval sentence at the end.
The argument I did not want to lose is the prior-art table right above this: NUnit, xUnit v3 and TUnit all call it Explicit, so someone arriving from any of them searches for [Explicit]. Against that, your point stands on its own merits: OnDemand describes the behaviour this document spends most of its length defining, and Explicit describes nothing unless you already know the term. I do not think one of those obviously beats the other, and it is not a call to make by whoever edits the file last. It changes every surface at once and it cannot be revised after the API ships, so it belongs to approval.
Manual is rejected in the text under either name, for the reason you gave.
If you want to just decide it now, say the word and I will do the rename in one commit.
🤖
|
|
||
| ```csharp | ||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] | ||
| public sealed class ExplicitAttribute : Attribute |
There was a problem hiding this comment.
Could OnDemandAttribute derive from ConditionBaseAttribute rather than directly from Attribute? This is another declarative execution gate that produces a skipped result, and the existing hierarchy already includes the unconditional IgnoreAttribute.
To preserve compatibility, we could add a non-nullable virtual EvaluateCondition(ConditionEvaluationContext context) whose base implementation delegates to IsConditionMet. OnDemandAttribute.IsConditionMet would return false so older adapters fail closed, while new adapters override EvaluateCondition to use context.IsTestActivated. New adapters would always call the method, and existing conditions would continue through the property.
This should remain separate from TestMethodAttribute so it composes with STA, UI, and user-defined test-method attributes and can also apply at class, data-source, and data-row scopes. The RFC would still keep its discovery metadata and early gate so unactivated tests are stopped before type or data-source loading.
There was a problem hiding this comment.
Also left open rather than decided, in a new Base type section and the second row of ## Open questions.
The paragraph you were replying to argued that a condition cannot know what the user asked for. Your EvaluateCondition(ConditionEvaluationContext) shape answers that, so I have rewritten it rather than leaving a rebuttal that no longer rebuts anything.
The part I had missed, and which is the strongest thing in your comment, is what IsConditionMet => false buys on an old adapter. AttributeExtensions.IsIgnored finds conditions by pattern-matching the base type, so an adapter that predates [Explicit] still finds the attribute through ConditionBaseAttribute and skips. That closes the pre-3.10.0 boundary the Compatibility section had written off as unfixable, from the framework side, which is a real argument and not a stylistic one. I have said so in both places.
The cost I want on the record before we choose: conditions resolve inside IsTestMethodRunnable, after TypeCache.GetTestMethodInfo, so the early gate has to stay regardless and the condition path becomes a second mechanism answering the same question later. Two mechanisms that must agree is the thing that will bite us, not the extra virtual. Worth it for the old-adapter behaviour, I think, but it is your call and it is a public API change on a shipped base class.
One detail for whoever writes it up: ConditionBaseAttribute is AttributeTargets.Class | AttributeTargets.Method, so it reaches two of the four scopes. Data sources and rows are not attributes, so they keep the capability interface and the row properties either way.
🤖
…ded-row hole Eight review threads, and five of them turned on a claim about the code that does not hold. VSTest does not evaluate an unset registered property as False: TestMethodFilter.PropertyValueProvider returns a value only when the TestCase carries the property and otherwise falls through to null, so a missing property already answers the same on both hosts, = does not match it and != does. The universal Explicit=False write existed only to paper over a divergence that was never there, and it was also writing a new trait onto every node of every MSTest-on-MTP run, because TestMetadataProperty is the transport for server mode traits, --list-tests JSON, TRX properties and OpenTelemetry tags. Explicit is now written only where it is declared, which makes the compatibility statement true as written and lets the test plan assert which surfaces the property reaches instead of claiming it reaches none. Unfolding clones the parent element, so an unfolded row inherited ExplicitFromSources=Some from a mixed-source parent, took the deferral, and met no per-source check downstream to stop it: Run All executed the twenty-minute row the RFC opens with. The gate now requires a folded parent as a normative third condition, and unfolding overwrites ExplicitFromSources per row from the source that produced it, so the deferral is unreachable from an unfolded row by two independent routes. The retry pipe cannot carry activation. RetryArgumentsBuilder adds --internal-retry-pipename to every attempt including the first, so attempt 1 of a UID run and attempt 2 of a retried Run All look identical. Activation now travels as --internal-retry-activation, and its absence, reachable through extension version skew, activates nothing. Also extends the reserved-name rule to [TestCategory], which shares one TestMetadataProperty key space with traits and could otherwise bypass the reservation, and stops reserving ExplicitReason, which no built-in surface occupies now that the reason is off node metadata. Naming and the attribute base type are recorded as open questions rather than decided here. Both change every surface at once and neither survives shipping, so they belong to approval.
There was a problem hiding this comment.
🟡 Changes recommended
Critical cleanup and runtime compatibility concerns, plus explicit-state aggregation gaps, must be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
docs/RFCs/024-Explicit-Tests.md:755
- The table lists four fields, not three. Keeping the count accurate matters here because the following paragraphs distinguish the gate fields from the reporting reason.
an execution request fails to activate it. Each `UnitTestElement` carries three fields, because
- Files reviewed: 1/1 changed files
- Comments generated: 4
- Review effort level: Balanced
| `ExplicitAttribute` derives from `Attribute` above. Deriving it from `ConditionBaseAttribute` instead | ||
| is the alternative, and the shape that makes it work is a non-nullable virtual | ||
| `EvaluateCondition(ConditionEvaluationContext context)` on the base whose default implementation | ||
| returns `IsConditionMet`. `ExplicitAttribute.IsConditionMet` returns `false`, and it overrides | ||
| `EvaluateCondition` to answer from `context.IsTestActivated`. New adapters call the method, existing |
| property. A `[TestCategory]` or a `[TestProperty]` whose name matches the reserved name in any casing | ||
| is written to none of the metadata surface, the categories, or the traits, and discovery reports a | ||
| warning naming the test, so the built-in value is the only one either host can match. |
|
|
||
| | Field | Meaning | Read by | | ||
| | --- | --- | --- | | ||
| | `IsExplicit` | the class, the method, or any source declares it | filters, node metadata, VSTest properties, reporting | |
| | `IsExplicit` | the class, the method, or any source declares it | filters, node metadata, VSTest properties, reporting | | ||
| | `ExplicitFromDeclaration` | the class or the method declares it | the execution gate in `UnitTestRunner` | | ||
| | `ExplicitFromSources` | what the data sources say: `None`, `All`, or `Some` | the execution gate in `UnitTestRunner` | | ||
| | `ExplicitReason` | see below, and none whenever the sources a gated parent stands for disagree | reporting | |
Adds
docs/RFCs/024-Explicit-Tests.md, a design for[Explicit]in MSTest that behaves the same on VSTest and Microsoft.Testing.Platform.Explicit tests are always discovered and displayed, but a broad Run All reports them as skipped. They run only when the request positively selects them. To make that precise without guessing at IDE intent, the RFC splits every request into a constraint ("is this test in the run?") and an activation ("did the user choose this test?"). An exclusion filter, a policy filter supplied by an extension, and an empty server selection can all constrain a run, but none of them can start a destructive test.
Activation is defined per request shape: concrete test-case and UID selections, a positive branch of a
TestCaseFilter/--filterexpression, and a discriminating segment of a tree-node or server graph filter. Anything whose activation cannot be determined activates nothing, so a future filter feature cannot quietly start running these tests before its semantics are designed.Also covers data rows and folding, inheritance and precedence against
[Ignore]and conditions, theExplicitTestModeoverride, retry behavior, reporting, old-adapter compatibility, implementation surfaces, and the test plan for both hosts.No production code changes. This is the design investigation asked for in the issue.
Open questions
Approval decides two things the RFC deliberately leaves open. Each changes every surface at once and neither can be revised after the API ships, so neither belongs to whoever edits the file last.
Explicitis what NUnit, xUnit v3 and TUnit call it, so someone arriving from any of them searches for[Explicit].OnDemanddescribes the behavior this document spends most of its length defining, whichExplicitdoes not do on its own. The document is written withExplicitbecause it needs one spelling to be readable, not because the question is settled.Manualis rejected under either name.Attribute, orConditionBaseAttributewith a virtualEvaluateCondition. The second closes the pre-3.10.0 adapter boundary from the framework side, because an old adapter finds the attribute through the base type and fails closed. It costs a public API change on a shipped base class and a second mechanism answering the same question after type load, since the early gate has to stay regardless.Beyond those, approval covers the public API, the positive-filter activation model, the discriminating segment rule for tree node and graph filters, the three
ExplicitTestModevalues, and the documented legacy VSTest boundary.Design for #5346
🤖