Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,38 @@
using Microsoft.Testing.Extensions.VSTestBridge.CommandLine;
using Microsoft.Testing.Platform;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Requests;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;

namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;

internal abstract class ContextAdapterBase
{
protected ContextAdapterBase(ICommandLineOptions commandLineOptions)
protected ContextAdapterBase(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter)
{
RunSettings = runSettings;

RoslynDebug.Assert(runSettings.SettingsXml is not null);

string? filterFromRunsettings = XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("TestCaseFilter")?.Value;
string? filterFromCommandLineOption = null;
if (commandLineOptions.TryGetOptionArgumentList(
TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName,
out string[]? filterExpressions)
&& filterExpressions is not null
&& filterExpressions.Length == 1)
{
FilterExpressionWrapper = new(filterExpressions[0]);
filterFromCommandLineOption = filterExpressions[0];
}

HandleFilter(filter, filterFromRunsettings, filterFromCommandLineOption);
}

protected FilterExpressionWrapper? FilterExpressionWrapper { get; set; }
public IRunSettings? RunSettings { get; }

private FilterExpressionWrapper? FilterExpressionWrapper { get; set; }

// NOTE: Implementation is borrowed from VSTest
// MSTest relies on this method existing and access it through reflection: https://github.com/microsoft/testfx/blob/main/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs#L115
Expand Down Expand Up @@ -61,4 +73,110 @@ protected ContextAdapterBase(ICommandLineOptions commandLineOptions)

return adapterSpecificTestCaseFilter;
}

private void HandleFilter(ITestExecutionFilter? filter, string? filterFromRunsettings, string? filterFromCommandLineOption)
{
// No filters at all, we can return immediately as there is nothing to do.
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(filterBuilder.ToString());
}

// Local functions
static void AppendFilter(string? filter, StringBuilder builder)
Comment thread
Youssef1313 marked this conversation as resolved.
{
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(')');
}

// We use heuristic to understand if the filter should be a TestCaseId or FullyQualifiedName.
// We know that in VSTest TestCaseId is a GUID and FullyQualifiedName is a string.
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 '~':
// If the symbol is not escaped, add an escape character.
if (i - 1 < 0 || currentTestNodeUid.Value[k - 1] != '\\')
{
filter.Append('\\');
}

filter.Append(currentChar);
break;

default:
filter.Append(currentChar);
break;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Requests;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;

namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
Expand All @@ -11,8 +12,8 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
/// </summary>
internal sealed class DiscoveryContextAdapter : ContextAdapterBase, IDiscoveryContext
{
public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings? runSettings)
: base(commandLineOptions) => RunSettings = runSettings;

public IRunSettings? RunSettings { get; }
public DiscoveryContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter)
: base(commandLineOptions, runSettings, filter)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using Microsoft.Testing.Platform;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Requests;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;

Expand All @@ -14,17 +13,13 @@ namespace Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
/// </summary>
internal sealed class RunContextAdapter : ContextAdapterBase, IRunContext
{
public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter? filter)
: base(commandLineOptions)
public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings runSettings, ITestExecutionFilter filter)
: base(commandLineOptions, runSettings, filter)
{
RoslynDebug.Assert(runSettings.SettingsXml is not null);

RunSettings = runSettings;

// Parse and take the results directory from the runsettings.
TestRunDirectory = XElement.Parse(runSettings.SettingsXml).Descendants("ResultsDirectory").SingleOrDefault()?.Value;

HandleFilter(filter);
TestRunDirectory = XDocument.Parse(runSettings.SettingsXml).Element("RunSettings")?.Element("RunConfiguration")?.Element("ResultsDirectory")?.Value;
}

// NOTE: Always false as it's TPv2 oriented and so not applicable to TA.
Expand All @@ -50,76 +45,4 @@ public RunContextAdapter(ICommandLineOptions commandLineOptions, IRunSettings ru

/// <inheritdoc />
public string? SolutionDirectory { get; }

/// <inheritdoc />
public IRunSettings? RunSettings { get; }

private void HandleFilter(ITestExecutionFilter? filter)
{
// TODO: Handle TreeNodeFilter
switch (filter)
{
case TestNodeUidListFilter testNodeUidListFilter:
FilterExpressionWrapper = new(CreateFilter(testNodeUidListFilter.TestNodeUids));
break;

default:
break;
}
}

// We use heuristic to understand if the filter should be a TestCaseId or FullyQualifiedName.
// We know that in VSTest TestCaseId is a GUID and FullyQualifiedName is a string.
private static string CreateFilter(TestNodeUid[] testNodesUid)
{
StringBuilder filter = new();

for (int i = 0; i < testNodesUid.Length; i++)
{
if (Guid.TryParse(testNodesUid[i].Value, out Guid guid))
{
filter.Append("Id=");
filter.Append(guid.ToString());
}
else
{
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 '~':
// If the symbol is not escaped, add an escape character.
if (i - 1 < 0 || currentTestNodeUid.Value[k - 1] != '\\')
{
filter.Append('\\');
}

filter.Append(currentChar);
break;

default:
filter.Append(currentChar);
break;
}
}
}

if (i != testNodesUid.Length - 1)
{
filter.Append('|');
}
}

return filter.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ internal sealed class RunSettingsAdapter : IRunSettings
"TargetFrameworkVersion",
"TargetPlatform",
"TestAdaptersPaths",
"TestCaseFilter",
"TestSessionTimeout",
"TreatNoTestsAsError",
"TreatTestAdapterErrorsAsWarnings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static VSTestDiscoverTestExecutionRequest CreateRequest(

ICommandLineOptions commandLineOptions = serviceProvider.GetRequiredService<ICommandLineOptions>();
RunSettingsAdapter runSettings = new(commandLineOptions, fileSystem, configuration, clientInfo, loggerFactory, messageLogger);
DiscoveryContextAdapter discoveryContext = new(commandLineOptions, runSettings);
DiscoveryContextAdapter discoveryContext = new(commandLineOptions, runSettings, discoverTestExecutionRequest.Filter);

ITestApplicationModuleInfo testApplicationModuleInfo = serviceProvider.GetTestApplicationModuleInfo();
IMessageBus messageBus = serviceProvider.GetRequiredService<IMessageBus>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ public async Task UnsupportedRunSettingsEntriesAreFlagged(string tfm)
testHostResult.AssertOutputContains("Runsettings attribute 'TargetFrameworkVersion' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputContains("Runsettings attribute 'TargetPlatform' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputContains("Runsettings attribute 'TestAdaptersPaths' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputContains("Runsettings attribute 'TestCaseFilter' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputContains("Runsettings attribute 'TestSessionTimeout' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputContains("Runsettings attribute 'TreatNoTestsAsError' is not supported by Microsoft.Testing.Platform and will be ignored");
testHostResult.AssertOutputDoesNotContain("Runsettings attribute 'TestCaseFilter' is not supported by Microsoft.Testing.Platform and will be ignored");
}

[TestMethod]
Expand Down
Loading