// 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; }