// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.Testing.Extensions.VSTestBridge.CommandLine;
using Microsoft.Testing.Extensions.VSTestBridge.ObjectModel;
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;
using Moq;
namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel;
///
/// Tests for the filter building logic in (exercised through
/// ), most notably the private BuildFilter method that turns a
/// coming from the server protocol into a VSTest
/// TestCaseFilter expression.
///
[TestClass]
public sealed class RunContextAdapterFilterTests
{
private const string EmptyRunSettings =
"""
""";
[TestMethod]
public void GetTestCaseFilter_WithSecondNodeContainingPipe_KeepsBothNodesAndEscapesPipe()
{
// A later node whose name contains '|' must be emitted as an escaped literal ('\|') so it
// stays part of its FullyQualifiedName value, while the '|' that joins the two clauses remains
// the OR operator.
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("Namespace.MyClass.MyTest", "PrintArg(\"as|\")"));
string res = GetFilterValue(adapter);
Assert.AreEqual("(FullyQualifiedName=Namespace.MyClass.MyTest|FullyQualifiedName=PrintArg\\(\"as\\|\"\\))", res);
}
[TestMethod]
public void GetTestCaseFilter_WithNopFilter_AndNoRunSettingsOrCommandLineFilter_ReturnsNull()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, new NopFilter());
Assert.IsNull(adapter.GetTestCaseFilter(null, _ => null));
}
[TestMethod]
public void GetTestCaseFilter_WithSingleFullyQualifiedNameNode_BuildsFullyQualifiedNameFilter()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("Namespace.MyClass.MyTest"));
Assert.AreEqual("(FullyQualifiedName=Namespace.MyClass.MyTest)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithEmptyNodeList_BuildsMatchNoneFilter()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter());
Assert.AreEqual(
"(FullyQualifiedName=__MTP_EMPTY_UID_FILTER__&FullyQualifiedName!=__MTP_EMPTY_UID_FILTER__)",
GetFilterValue(adapter));
Assert.IsFalse(MatchesFullyQualifiedName(adapter, "__MTP_EMPTY_UID_FILTER__"));
Assert.IsFalse(MatchesFullyQualifiedName(adapter, "A.B.Test"));
}
[TestMethod]
public void GetTestCaseFilter_WithMultipleNodes_JoinsWithOrOperator()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("A.B.Test1", "C.D.Test2"));
Assert.AreEqual("(FullyQualifiedName=A.B.Test1|FullyQualifiedName=C.D.Test2)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithGuidNode_BuildsIdFilter()
{
var guid = new Guid("12345678-1234-1234-1234-1234567890ab");
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter(guid.ToString()));
Assert.AreEqual($"(Id={guid})", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithGuidShapedFullyQualifiedName_AndUseFullyQualifiedNameAsUid_BuildsFullyQualifiedNameFilter()
{
// Regression for #9784: when the UID was produced from TestCase.FullyQualifiedName
// (useFullyQualifiedNameAsUid = true), a name that happens to be GUID-shaped (e.g. a
// data-driven test whose display name is exactly a GUID string) must still be emitted as a
// FullyQualifiedName= clause, not misclassified as an Id= clause by a Guid.TryParse heuristic.
const string GuidShapedName = "12345678-1234-1234-1234-1234567890ab";
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter(GuidShapedName), useFullyQualifiedNameAsUid: true);
Assert.AreEqual($"(FullyQualifiedName={GuidShapedName})", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithMixedGuidAndNameNodes_BuildsIdAndFullyQualifiedNameFilters()
{
var guid = new Guid("12345678-1234-1234-1234-1234567890ab");
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter(guid.ToString(), "A.B.Test"));
Assert.AreEqual($"(Id={guid}|FullyQualifiedName=A.B.Test)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithSpecialCharactersInName_EscapesFilterOperators()
{
// The pipe, parentheses, ampersand, equals, bang, tilde and backslash are all TestCaseFilter
// operators and must be escaped so they are treated as literals (regression for tests whose
// display name contains such characters, e.g. NUnit [TestCase("as|")]).
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("Ns.PrintArg(\"as|\")"));
Assert.AreEqual("(FullyQualifiedName=Ns.PrintArg\\(\"as\\|\"\\))", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithAllOperatorCharacters_EscapesEachOfThem()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("a\\b(c)d&e|f=g!h~i"));
Assert.AreEqual("(FullyQualifiedName=a\\\\b\\(c\\)d\\&e\\|f\\=g\\!h\\~i)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithRunSettingsTestCaseFilter_UsesRunSettingsFilter()
{
string runSettings =
"""
Category=Fast
""";
RunContextAdapter adapter = CreateAdapter(runSettings, new NopFilter());
Assert.AreEqual("(Category=Fast)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithCommandLineFilter_UsesCommandLineFilter()
{
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, new NopFilter(), commandLineFilter: "Category=Slow");
Assert.AreEqual("(Category=Slow)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WhenCommandLineFilterLookupFails_IgnoresReturnedArguments()
{
RunContextAdapter adapter = CreateAdapter(
EmptyRunSettings,
new NopFilter(),
commandLineFilter: "Category=Ignored",
commandLineFilterOptionFound: false);
Assert.IsNull(adapter.GetTestCaseFilter(null, _ => null));
}
[TestMethod]
public void GetTestCaseFilter_WithRunSettingsAndNodeFilter_CombinesWithAndOperator()
{
string runSettings =
"""
Category=Fast
""";
RunContextAdapter adapter = CreateAdapter(runSettings, CreateUidFilter("A.B.Test"));
Assert.AreEqual("(Category=Fast) & (FullyQualifiedName=A.B.Test)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithAndComposite_TranslatesChildrenRecursively()
{
CompositeTestExecutionFilter filter = new(
TestExecutionFilterOperator.And,
CreateUidFilter("A.B.Test1", "A.B.Test2"),
CreateUidFilter("A.B.Test2", "A.B.Test3"));
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, filter);
Assert.AreEqual(
"(FullyQualifiedName=A.B.Test1|FullyQualifiedName=A.B.Test2) & (FullyQualifiedName=A.B.Test2|FullyQualifiedName=A.B.Test3)",
GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithRunSettingsAndComposite_PreservesAndSemantics()
{
string runSettings =
"""
Category=Fast
""";
CompositeTestExecutionFilter filter = new(
TestExecutionFilterOperator.And,
new NopFilter(),
CreateUidFilter("A.B.Test"));
RunContextAdapter adapter = CreateAdapter(runSettings, filter);
Assert.AreEqual("(Category=Fast) & (FullyQualifiedName=A.B.Test)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithCompositeContainingOnlyNopFilters_ReturnsNull()
{
CompositeTestExecutionFilter filter = new(
TestExecutionFilterOperator.And,
new NopFilter(),
new NopFilter());
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, filter);
Assert.IsNull(adapter.GetTestCaseFilter(null, _ => null));
}
[TestMethod]
public void GetTestCaseFilter_DefaultConstructor_WithGuidUid_BuildsIdFilter()
{
var runSettings = new Mock();
runSettings.Setup(x => x.SettingsXml).Returns(EmptyRunSettings);
var commandLineOptions = new Mock();
string[]? commandLineFilterArguments = null;
commandLineOptions
.Setup(x => x.TryGetOptionArgumentList(TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName, out commandLineFilterArguments))
.Returns(false);
var guid = new Guid("12345678-1234-1234-1234-1234567890ab");
RunContextAdapter adapter = new(commandLineOptions.Object, runSettings.Object, CreateUidFilter(guid.ToString()));
Assert.AreEqual($"(Id={guid})", GetFilterValue(adapter));
}
[TestMethod]
public void Constructor_WithTreeNodeFilter_ThrowsActionableError()
{
NotSupportedException exception = Assert.ThrowsExactly(
() => CreateAdapter(EmptyRunSettings, new TreeNodeFilter("/Tests/**")));
Assert.Contains(nameof(TreeNodeFilter), exception.Message);
Assert.Contains("VSTestBridge", exception.Message);
}
[TestMethod]
public void Constructor_WithCustomFilter_ThrowsActionableError()
{
NotSupportedException exception = Assert.ThrowsExactly(
() => CreateAdapter(EmptyRunSettings, new CustomFilter()));
Assert.Contains(nameof(CustomFilter), exception.Message);
Assert.Contains("VSTestBridge", exception.Message);
}
[TestMethod]
public void GetTestCaseFilter_WithRunSettingsAndCommandLineFilter_CombinesWithAndOperator()
{
string runSettings =
"""
Category=Fast
""";
RunContextAdapter adapter = CreateAdapter(runSettings, new NopFilter(), commandLineFilter: "Priority=1");
Assert.AreEqual("(Category=Fast) & (Priority=1)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithSingleNodeStartingWithSpecialCharacter_DoesNotThrowAndEscapes()
{
// Companion to the multi-node regression above, exhaustive over the index dimension: the
// single-node case (i == 0) never threw under the old code because the "i - 1 < 0" guard
// short-circuited before the bogus Value[k - 1] read. It must still escape the leading operator.
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("(weird"));
Assert.AreEqual("(FullyQualifiedName=\\(weird)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithSecondNodeStartingWithSpecialCharacter_DoesNotThrowAndEscapes()
{
// Regression: the "already-escaped" guard in BuildFilter used the node index (i) instead of
// the character index (k) when looking back at the previous character. For any node after the
// first (i > 0) whose first character (k == 0) is a filter operator, this evaluated
// Value[k - 1] == Value[-1] and threw IndexOutOfRangeException.
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("A.B.Test", "(weird"));
Assert.AreEqual("(FullyQualifiedName=A.B.Test|FullyQualifiedName=\\(weird)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithOperator_EscapesOperator()
{
// A bare operator ('|') in the name must be escaped so it stays a literal instead of being
// parsed as an OR that splits the clause.
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("A.B|C"));
Assert.AreEqual("(FullyQualifiedName=A.B\\|C)", GetFilterValue(adapter));
}
[TestMethod]
public void GetTestCaseFilter_WithBackslashFollowedBySpecialCharacter_EscapesBoth()
{
// Regression: the buggy "already-escaped" guard treated the operator following a literal
// backslash as if it were already escaped, so it emitted the operator un-escaped. A raw
// backslash in the name must be escaped to "\\" AND the following operator must still be
// escaped, otherwise the operator (e.g. '|') is parsed as an OR and the clause is split.
RunContextAdapter adapterBackslash = CreateAdapter(EmptyRunSettings, CreateUidFilter("A.B\\|C"));
Assert.AreEqual("(FullyQualifiedName=A.B\\\\\\|C)", GetFilterValue(adapterBackslash));
}
[TestMethod]
public void GetTestCaseFilter_WithSpecialCharactersInName_RoundTripsAndMatchesOnlyExactName()
{
// Verify the full escape -> parse -> match path (not just the emitted string): a name full of
// filter operators must, once escaped, parse back to a filter that matches a test case whose
// FullyQualifiedName is exactly that name and rejects any other name.
const string name = "Ns.PrintArg(\"as|\")";
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter(name));
Assert.IsTrue(MatchesFullyQualifiedName(adapter, name));
Assert.IsFalse(MatchesFullyQualifiedName(adapter, "Ns.PrintArg(\"as\")"));
}
[TestMethod]
public void GetTestCaseFilter_WithBackslashFollowedBySpecialCharacter_RoundTripsAndMatchesOnlyExactName()
{
// Regression guard for the round trip: if the backslash-then-operator sequence were under-escaped
// the parser would split the value at the operator, so the filter would no longer match the exact
// name (and might match a truncated one).
const string name = "A.B\\|C";
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter(name));
Assert.IsTrue(MatchesFullyQualifiedName(adapter, name));
Assert.IsFalse(MatchesFullyQualifiedName(adapter, "A.B"));
}
[TestMethod]
public void GetTestCaseFilter_WithMultipleNodes_MatchesEitherNodeButNotOthers()
{
// The '|' joining the two clauses must stay an OR operator while a '|' inside a name stays a
// literal, so the filter matches each node's exact name (including the special-character one) and
// nothing else.
RunContextAdapter adapter = CreateAdapter(EmptyRunSettings, CreateUidFilter("A.B.Test1", "PrintArg(\"as|\")"));
Assert.IsTrue(MatchesFullyQualifiedName(adapter, "A.B.Test1"));
Assert.IsTrue(MatchesFullyQualifiedName(adapter, "PrintArg(\"as|\")"));
Assert.IsFalse(MatchesFullyQualifiedName(adapter, "A.B.Test2"));
}
private static TestNodeUidListFilter CreateUidFilter(params string[] uids)
=> new([.. uids.Select(uid => new TestNodeUid(uid))]);
private static string GetFilterValue(RunContextAdapter adapter)
{
ITestCaseFilterExpression? filterExpression = adapter.GetTestCaseFilter(null, _ => null);
Assert.IsNotNull(filterExpression);
return filterExpression.TestCaseFilterValue;
}
private static bool MatchesFullyQualifiedName(RunContextAdapter adapter, string fullyQualifiedName)
{
ITestCaseFilterExpression? filterExpression = adapter.GetTestCaseFilter(null, _ => null);
Assert.IsNotNull(filterExpression);
var testCase = new TestCase(fullyQualifiedName, new Uri("executor://mstest"), "source.dll");
return filterExpression.MatchTestCase(
testCase,
propertyName => string.Equals(propertyName, "FullyQualifiedName", StringComparison.OrdinalIgnoreCase)
? fullyQualifiedName
: null);
}
private static RunContextAdapter CreateAdapter(
string runSettingsXml,
ITestExecutionFilter filter,
string? commandLineFilter = null,
bool useFullyQualifiedNameAsUid = false,
bool commandLineFilterOptionFound = true)
{
var runSettings = new Mock();
runSettings.Setup(x => x.SettingsXml).Returns(runSettingsXml);
var commandLineOptions = new Mock();
string[]? commandLineFilterArguments = commandLineFilter is null ? null : [commandLineFilter];
commandLineOptions
.Setup(x => x.TryGetOptionArgumentList(TestCaseFilterCommandLineOptionsProvider.TestCaseFilterOptionName, out commandLineFilterArguments))
.Returns(commandLineFilter is not null && commandLineFilterOptionFound);
return new RunContextAdapter(commandLineOptions.Object, runSettings.Object, filter, useFullyQualifiedNameAsUid);
}
private sealed class CustomFilter : ITestExecutionFilter;
}