-
Notifications
You must be signed in to change notification settings - Fork 307
Native MTP framework for MSTest — Phases 3–5 (native filter/runsettings/context, opt-in) #9743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+766
−42
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
332646e
Phase 3-5: native MTP framework for MSTest (filter/runsettings/contex…
Evangelink be709b3
Address PR review: UTF-8 BOM on new files, LINQ Where in runsettings …
Evangelink a666b29
Address PR review: defensive guards + fix filter-escape index bug
Evangelink e89c522
Address PR review: --settings list, localized session message, Config…
Evangelink File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
225 changes: 225 additions & 0 deletions
225
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFilterContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| #if !WINDOWS_UWP | ||
| using Microsoft.Testing.Platform.CommandLine; | ||
| using Microsoft.Testing.Platform.Extensions.Messages; | ||
| using Microsoft.Testing.Platform.Requests; | ||
| using Microsoft.VisualStudio.TestPlatform.Common.Filtering; | ||
| using Microsoft.VisualStudio.TestPlatform.ObjectModel; | ||
| using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; | ||
|
|
||
| namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestingPlatformAdapter; | ||
|
|
||
| /// <summary> | ||
| /// A native Microsoft.Testing.Platform (MTP) filter context for MSTest. It builds a VSTest | ||
| /// <see cref="ITestCaseFilterExpression"/> (which MSTest's <c>TestMethodFilter</c> evaluates over | ||
| /// <c>UnitTestElement</c>s) directly from the MTP <see cref="ITestExecutionFilter"/>, the <c>--filter</c> | ||
| /// command-line option and the runsettings <c><TestCaseFilter></c> — without going through the VSTest bridge's | ||
| /// context adapters. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This mirrors, for the MSTest native path, what <c>ContextAdapterBase</c> / <c>RunContextAdapter</c> / | ||
| /// <c>DiscoveryContextAdapter</c> do in <c>Microsoft.Testing.Extensions.VSTestBridge</c>. The filter-expression | ||
| /// parsing itself reuses the VSTest <c>Microsoft.TestPlatform.Filter.Source</c> package, exactly like the bridge. | ||
| /// </remarks> | ||
| [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] | ||
| internal abstract class MSTestFilterContextBase | ||
| { | ||
| // Keep in sync with the bridge's TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName. | ||
| private const string TestCaseFilterOptionName = "filter"; | ||
|
|
||
| protected MSTestFilterContextBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) | ||
| { | ||
| RunSettings = runSettings; | ||
|
|
||
| string? filterFromRunsettings = runSettings.SettingsXml is null | ||
| ? null | ||
| : XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("TestCaseFilter")?.Value; | ||
|
|
||
| string? filterFromCommandLineOption = null; | ||
| if (commandLineOptions.TryGetOptionArgumentList(TestCaseFilterOptionName, out string[]? filterExpressions) | ||
| && filterExpressions is not null | ||
| && filterExpressions.Length == 1) | ||
| { | ||
| filterFromCommandLineOption = filterExpressions[0]; | ||
| } | ||
|
|
||
| HandleFilter(filter, filterFromRunsettings, filterFromCommandLineOption); | ||
| } | ||
|
|
||
| public IRunSettings? RunSettings { get; } | ||
|
|
||
| private FilterExpressionWrapper? FilterExpressionWrapper { get; set; } | ||
|
|
||
| private sealed class MSTestFilterExpression : ITestCaseFilterExpression | ||
| { | ||
| private readonly TestCaseFilterExpression _testCaseFilterExpression; | ||
|
|
||
| public MSTestFilterExpression(TestCaseFilterExpression testCaseFilterExpression) | ||
| => _testCaseFilterExpression = testCaseFilterExpression; | ||
|
|
||
| public string TestCaseFilterValue | ||
| => _testCaseFilterExpression.TestCaseFilterValue; | ||
|
|
||
| public bool MatchTestCase(TestCase testCase, Func<string, object?> propertyValueProvider) | ||
| => _testCaseFilterExpression.MatchTestCase(propertyValueProvider); | ||
| } | ||
|
|
||
| // NOTE: MSTest.TestAdapter's TestMethodFilter accesses this method via reflection on the discovery context | ||
| // (see GetTestCaseFilterFromDiscoveryContext) and directly on the run context. | ||
| #pragma warning disable IDE0060 // Remove unused parameter - kept for the VSTest GetTestCaseFilter contract shape. | ||
| public ITestCaseFilterExpression? GetTestCaseFilter( | ||
| IEnumerable<string>? supportedProperties, | ||
| Func<string, TestProperty?> propertyProvider) | ||
| #pragma warning restore IDE0060 | ||
| => FilterExpressionWrapper is null | ||
| ? null | ||
| : !string.IsNullOrEmpty(FilterExpressionWrapper.ParseError) | ||
| ? throw new TestPlatformFormatException(FilterExpressionWrapper.ParseError, FilterExpressionWrapper.FilterString) | ||
| : new MSTestFilterExpression(new TestCaseFilterExpression(FilterExpressionWrapper)); | ||
|
|
||
| private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunsettings, string? filterFromCommandLineOption) | ||
| { | ||
| if (filter is null or NopFilter | ||
| && filterFromRunsettings is null | ||
| && filterFromCommandLineOption is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var filterBuilder = new StringBuilder(); | ||
|
|
||
| AppendFilter(filterFromRunsettings, filterBuilder); | ||
| AppendFilter(filterFromCommandLineOption, filterBuilder); | ||
|
|
||
| if (filter is TestNodeUidListFilter testNodeUidListFilter) | ||
| { | ||
| StartFilter(filterBuilder); | ||
| BuildFilter(testNodeUidListFilter.TestNodeUids, filterBuilder); | ||
| EndFilter(filterBuilder); | ||
| } | ||
|
|
||
| if (filterBuilder.Length > 0) | ||
| { | ||
| FilterExpressionWrapper = new FilterExpressionWrapper(filterBuilder.ToString()); | ||
| } | ||
|
|
||
| static void AppendFilter(string? filter, StringBuilder builder) | ||
| { | ||
| if (filter is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| StartFilter(builder); | ||
| builder.Append(filter); | ||
| EndFilter(builder); | ||
| } | ||
|
|
||
| static void StartFilter(StringBuilder builder) | ||
| { | ||
| if (builder.Length > 0) | ||
| { | ||
| builder.Append(" & ("); | ||
| } | ||
| else | ||
| { | ||
| builder.Append('('); | ||
| } | ||
| } | ||
|
|
||
| static void EndFilter(StringBuilder builder) | ||
| => builder.Append(')'); | ||
| } | ||
|
|
||
| // Heuristic borrowed from the bridge: a GUID TestNodeUid maps to a VSTest Id filter, everything else to a | ||
| // (escaped) FullyQualifiedName filter. | ||
| private static void BuildFilter(TestNodeUid[] testNodesUid, StringBuilder filter) | ||
| { | ||
| for (int i = 0; i < testNodesUid.Length; i++) | ||
| { | ||
| if (i > 0) | ||
| { | ||
| filter.Append('|'); | ||
| } | ||
|
|
||
| if (Guid.TryParse(testNodesUid[i].Value, out Guid guid)) | ||
| { | ||
| filter.Append("Id="); | ||
| filter.Append(guid.ToString()); | ||
| continue; | ||
| } | ||
|
|
||
| TestNodeUid currentTestNodeUid = testNodesUid[i]; | ||
| filter.Append("FullyQualifiedName="); | ||
| for (int k = 0; k < currentTestNodeUid.Value.Length; k++) | ||
| { | ||
| char currentChar = currentTestNodeUid.Value[k]; | ||
| switch (currentChar) | ||
| { | ||
| case '\\': | ||
| case '(': | ||
| case ')': | ||
| case '&': | ||
| case '|': | ||
| case '=': | ||
| case '!': | ||
| case '~': | ||
| // Escape the special char if it is not already escaped. Note: index into k-1 (the previous | ||
| // character in this UID value), not i-1. | ||
| if (k - 1 < 0 || currentTestNodeUid.Value[k - 1] != '\\') | ||
| { | ||
| filter.Append('\\'); | ||
| } | ||
|
|
||
| filter.Append(currentChar); | ||
| break; | ||
|
|
||
| default: | ||
| filter.Append(currentChar); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Native MTP implementation of the VSTest <see cref="IRunContext"/> for the MSTest native path. | ||
| /// </summary> | ||
| [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] | ||
| internal sealed class MSTestRunContext : MSTestFilterContextBase, IRunContext | ||
| { | ||
| public MSTestRunContext(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) | ||
| : base(commandLineOptions, runSettings, filter) | ||
| => TestRunDirectory = runSettings.SettingsXml is null | ||
| ? null | ||
| : XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("ResultsDirectory")?.Value; | ||
|
|
||
| // TPv2-oriented flags that do not apply to the platform adapter (mirrors RunContextAdapter). | ||
| public bool KeepAlive { get; } | ||
|
|
||
| public bool InIsolation { get; } | ||
|
|
||
| public bool IsDataCollectionEnabled { get; } | ||
|
|
||
| public bool IsBeingDebugged => Debugger.IsAttached; | ||
|
|
||
| public string? TestRunDirectory { get; } | ||
|
|
||
| public string? SolutionDirectory { get; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Native MTP implementation of the VSTest <see cref="IDiscoveryContext"/> for the MSTest native path. | ||
| /// </summary> | ||
| [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] | ||
| internal sealed class MSTestDiscoveryContext : MSTestFilterContextBase, IDiscoveryContext | ||
| { | ||
| public MSTestDiscoveryContext(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter) | ||
| : base(commandLineOptions, runSettings, filter) | ||
| { | ||
| } | ||
| } | ||
| #endif | ||
78 changes: 78 additions & 0 deletions
78
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestFrameworkHandle.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| #if !WINDOWS_UWP | ||
| using Microsoft.Testing.Platform.Extensions; | ||
| using Microsoft.Testing.Platform.Extensions.OutputDevice; | ||
| using Microsoft.Testing.Platform.OutputDevice; | ||
| using Microsoft.VisualStudio.TestPlatform.ObjectModel; | ||
| using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; | ||
| using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; | ||
|
|
||
| namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.TestingPlatformAdapter; | ||
|
|
||
| /// <summary> | ||
| /// A native Microsoft.Testing.Platform (MTP) <see cref="IFrameworkHandle"/> for the MSTest native path. It only | ||
| /// forwards diagnostic messages to the MTP <see cref="IOutputDevice"/>; test results are reported natively through | ||
| /// the <c>MtpTestResultRecorder</c>, so the VSTest result-recording members are intentionally no-ops. | ||
| /// </summary> | ||
| [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] | ||
| internal sealed class MSTestFrameworkHandle : IFrameworkHandle, IOutputDeviceDataProducer | ||
| { | ||
| private readonly IOutputDevice _outputDevice; | ||
| private readonly IExtension _extension; | ||
| private readonly CancellationToken _cancellationToken; | ||
|
|
||
| public MSTestFrameworkHandle(IOutputDevice outputDevice, IExtension extension, CancellationToken cancellationToken) | ||
| { | ||
| _outputDevice = outputDevice; | ||
| _extension = extension; | ||
| _cancellationToken = cancellationToken; | ||
| } | ||
|
|
||
| public bool EnableShutdownAfterTestRun { get; set; } | ||
|
|
||
| string IExtension.Uid => _extension.Uid; | ||
|
|
||
| string IExtension.Version => _extension.Version; | ||
|
|
||
| string IExtension.DisplayName => _extension.DisplayName; | ||
|
|
||
| string IExtension.Description => _extension.Description; | ||
|
|
||
| public int LaunchProcessWithDebuggerAttached(string filePath, string? workingDirectory, string? arguments, IDictionary<string, string?>? environmentVariables) | ||
| => -1; | ||
|
|
||
| // Results are reported natively through the recorder; the VSTest recording members are not used. | ||
| public void RecordResult(TestResult testResult) | ||
| { | ||
| } | ||
|
|
||
| public void RecordStart(TestCase testCase) | ||
| { | ||
| } | ||
|
|
||
| public void RecordEnd(TestCase testCase, TestOutcome outcome) | ||
| { | ||
| } | ||
|
|
||
| public void RecordAttachments(IList<AttachmentSet> attachmentSets) | ||
| { | ||
| } | ||
|
|
||
| public void SendMessage(TestMessageLevel testMessageLevel, string message) | ||
| { | ||
| IOutputDeviceData data = testMessageLevel switch | ||
| { | ||
| TestMessageLevel.Informational => new TextOutputDeviceData(message), | ||
| TestMessageLevel.Warning => new WarningMessageOutputDeviceData(message), | ||
| TestMessageLevel.Error => new ErrorMessageOutputDeviceData(message), | ||
| _ => throw new NotSupportedException($"Unsupported logging level '{testMessageLevel}'."), | ||
| }; | ||
|
|
||
| _outputDevice.DisplayAsync(this, data, _cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); | ||
| } | ||
|
Evangelink marked this conversation as resolved.
|
||
|
|
||
| Task<bool> IExtension.IsEnabledAsync() => _extension.IsEnabledAsync(); | ||
| } | ||
| #endif | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.