From dc89468676e7fb546a7fd3e792a6ed242bb751cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:51:54 +0000 Subject: [PATCH 1/7] Initial plan From ef1fd6a57117a0f30339faa02aaba8efa6ad3b79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:09:15 +0000 Subject: [PATCH 2/7] Implement empty TestCategory filtering for uncategorized tests Co-authored-by: nohwnd <5735905+nohwnd@users.noreply.github.com> --- .../ObjectModel/Condition.cs | 84 ++++++--- .../ObjectModel/FastFilter.cs | 17 +- .../TestMethod/TestCategoryAttribute.cs | 19 +- .../TestFilterTests.cs | 98 ++++++++++ .../ObjectModel/ConditionTests.cs | 170 ++++++++++++++++++ .../Attributes/TestCategoryAttributeTests.cs | 47 +++++ 6 files changed, 401 insertions(+), 34 deletions(-) create mode 100644 test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Attributes/TestCategoryAttributeTests.cs diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs index 9c072cdb0f..b3423c2279 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs @@ -80,8 +80,13 @@ internal bool Evaluate(Func propertyValueProvider) switch (Operation) { case Operation.Equal: + // Special case: empty string filter value matches null/empty/whitespace property (uncategorized tests) + if (string.IsNullOrWhiteSpace(Value)) + { + result = multiValue is null or { Length: 0 }; + } // if any value in multi-valued property matches 'this.Value', for Equal to evaluate true. - if (multiValue != null) + else if (multiValue != null) { foreach (string propertyValue in multiValue) { @@ -96,18 +101,27 @@ internal bool Evaluate(Func propertyValueProvider) break; case Operation.NotEqual: - // all values in multi-valued property should not match 'this.Value' for NotEqual to evaluate true. - result = true; - - // if value is null. - if (multiValue != null) + // Special case: empty string filter value matches null/empty property (uncategorized tests) + // So NotEqual to empty string should match tests WITH categories + if (string.IsNullOrWhiteSpace(Value)) { - foreach (string propertyValue in multiValue) + result = multiValue is not null and { Length: > 0 }; + } + else + { + // all values in multi-valued property should not match 'this.Value' for NotEqual to evaluate true. + result = true; + + // if value is null. + if (multiValue != null) { - result = result && !string.Equals(propertyValue, Value, StringComparison.OrdinalIgnoreCase); - if (!result) + foreach (string propertyValue in multiValue) { - break; + result = result && !string.Equals(propertyValue, Value, StringComparison.OrdinalIgnoreCase); + if (!result) + { + break; + } } } } @@ -115,8 +129,13 @@ internal bool Evaluate(Func propertyValueProvider) break; case Operation.Contains: + // Special case: empty string filter value matches null/empty property (uncategorized tests) + if (string.IsNullOrWhiteSpace(Value)) + { + result = multiValue is null or { Length: 0 }; + } // if any value in multi-valued property contains 'this.Value' for 'Contains' to be true. - if (multiValue != null) + else if (multiValue != null) { foreach (string propertyValue in multiValue) { @@ -132,18 +151,27 @@ internal bool Evaluate(Func propertyValueProvider) break; case Operation.NotContains: - // all values in multi-valued property should not contain 'this.Value' for NotContains to evaluate true. - result = true; - - if (multiValue != null) + // Special case: empty string filter value matches null/empty property (uncategorized tests) + // So NotContains empty string should match tests WITH categories + if (string.IsNullOrWhiteSpace(Value)) { - foreach (string propertyValue in multiValue) + result = multiValue is not null and { Length: > 0 }; + } + else + { + // all values in multi-valued property should not contain 'this.Value' for NotContains to evaluate true. + result = true; + + if (multiValue != null) { - RoslynDebug.Assert(propertyValue != null, "PropertyValue can not be null."); - result = result && !propertyValue.Contains(Value, StringComparison.OrdinalIgnoreCase); - if (!result) + foreach (string propertyValue in multiValue) { - break; + RoslynDebug.Assert(propertyValue != null, "PropertyValue can not be null."); + result = result && !propertyValue.Contains(Value, StringComparison.OrdinalIgnoreCase); + if (!result) + { + break; + } } } } @@ -177,16 +205,18 @@ internal static Condition Parse(string? conditionString) ThrownFormatExceptionForInvalidCondition(conditionString); } - for (int index = 0; index < 3; index++) + // Property name (parts[0]) and operator (parts[1]) must not be empty + if (RoslynString.IsNullOrWhiteSpace(parts[0]) || RoslynString.IsNullOrWhiteSpace(parts[1])) { - if (RoslynString.IsNullOrWhiteSpace(parts[index])) - { - ThrownFormatExceptionForInvalidCondition(conditionString); - } - - parts[index] = parts[index].Trim(); + ThrownFormatExceptionForInvalidCondition(conditionString); } + // parts[2] (value) can be empty to support filtering for uncategorized tests + // Trim all parts + parts[0] = parts[0].Trim(); + parts[1] = parts[1].Trim(); + parts[2] = parts[2].Trim(); + Operation operation = GetOperator(parts[1]); Condition condition = new(parts[0], operation, FilterHelper.Unescape(parts[2])); return condition; diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs index 1a8c3ebdb1..ec434ec2ec 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs @@ -46,9 +46,17 @@ internal bool Evaluate(Func propertyValueProvider) bool matched = false; foreach (string name in FilterProperties.Keys) { - // If there is no value corresponding to given name, treat it as unmatched. + // Special case: if filter contains empty string, check if property is null/empty (uncategorized) + bool hasEmptyStringFilter = FilterProperties[name].Any(string.IsNullOrWhiteSpace); + + // If there is no value corresponding to given name, treat it as unmatched unless filtering for empty string. if (!TryGetPropertyValue(name, propertyValueProvider, out string? singleValue, out string[]? multiValues)) { + if (hasEmptyStringFilter) + { + matched = true; + break; + } continue; } @@ -57,11 +65,16 @@ internal bool Evaluate(Func propertyValueProvider) string? value = PropertyValueRegex == null ? singleValue : ApplyRegex(singleValue); matched = value != null && FilterProperties[name].Contains(value); } - else + else if (multiValues is { Length: > 0 }) { IEnumerable? values = PropertyValueRegex == null ? multiValues : multiValues?.Select(ApplyRegex); matched = values?.Any(result => result != null && FilterProperties[name].Contains(result)) == true; } + else if (hasEmptyStringFilter) + { + // Empty array matches empty string filter + matched = true; + } if (matched) { diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/TestCategoryAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/TestCategoryAttribute.cs index fd4633692e..7f28b2d172 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/TestCategoryAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/TestCategoryAttribute.cs @@ -18,11 +18,20 @@ public sealed class TestCategoryAttribute : TestCategoryBaseAttribute /// public TestCategoryAttribute(string testCategory) { - List categories = - [ - testCategory - ]; - TestCategories = categories; + // Treat empty or whitespace-only strings as null/uncategorized + // This ensures empty categories are not added to the list + if (string.IsNullOrWhiteSpace(testCategory)) + { + TestCategories = []; + } + else + { + List categories = + [ + testCategory + ]; + TestCategories = categories; + } } /// diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs index 6f67a60835..75b2057dd9 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TestFilterTests.cs @@ -117,6 +117,91 @@ public async Task RunWithFilterFromRunsettings(string currentTfm) testHostResult.AssertExitCodeIs(ExitCodes.Success); } + [TestMethod] + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + public async Task RunWithFilter_EmptyTestCategory_MatchesUncategorizedTests(string currentTfm) + { + var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); + + // Filter for uncategorized tests using empty string + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter TestCategory=", cancellationToken: TestContext.CancellationToken); + + // Should match tests without any TestCategory + testHostResult.AssertOutputContains("Running test: NoCategoryTest"); + testHostResult.AssertOutputContains("Running test: EmptyCategoryTest"); + + // Should NOT match tests with categories + testHostResult.AssertOutputDoesNotContain("Running test: CategoryAOnly"); + testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); + testHostResult.AssertOutputDoesNotContain("Running test: CategoryAAndB"); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + } + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + public async Task RunWithFilter_EmptyTestCategoryNotEqual_MatchesCategorizedTests(string currentTfm) + { + var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); + + // Filter for tests that ARE categorized (not equal to empty) + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter TestCategory!=", cancellationToken: TestContext.CancellationToken); + + // Should match tests with categories + testHostResult.AssertOutputContains("Running test: CategoryAOnly"); + testHostResult.AssertOutputContains("Running test: CategoryBOnly"); + testHostResult.AssertOutputContains("Running test: CategoryAAndB"); + + // Should NOT match tests without categories + testHostResult.AssertOutputDoesNotContain("Running test: NoCategoryTest"); + testHostResult.AssertOutputDoesNotContain("Running test: EmptyCategoryTest"); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + } + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + public async Task RunWithFilter_EmptyTestCategoryOrSpecificCategory_MatchesBoth(string currentTfm) + { + var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); + + // Filter for uncategorized OR CategoryA + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter TestCategory=|TestCategory=CategoryA", cancellationToken: TestContext.CancellationToken); + + // Should match uncategorized tests + testHostResult.AssertOutputContains("Running test: NoCategoryTest"); + testHostResult.AssertOutputContains("Running test: EmptyCategoryTest"); + + // Should match CategoryA tests + testHostResult.AssertOutputContains("Running test: CategoryAOnly"); + testHostResult.AssertOutputContains("Running test: CategoryAAndB"); + + // Should NOT match CategoryB only + testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + } + + [TestMethod] + [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] + public async Task RunWithFilter_EmptyTestCategoryContains_MatchesUncategorizedTests(string currentTfm) + { + var testHost = TestHost.LocateFrom(AssetFixture.TargetAssetPath, AssetName, currentTfm); + + // Filter using contains operator with empty string + TestHostResult testHostResult = await testHost.ExecuteAsync("--filter TestCategory~", cancellationToken: TestContext.CancellationToken); + + // Should match tests without any TestCategory + testHostResult.AssertOutputContains("Running test: NoCategoryTest"); + testHostResult.AssertOutputContains("Running test: EmptyCategoryTest"); + + // Should NOT match tests with categories + testHostResult.AssertOutputDoesNotContain("Running test: CategoryAOnly"); + testHostResult.AssertOutputDoesNotContain("Running test: CategoryBOnly"); + + testHostResult.AssertExitCodeIs(ExitCodes.Success); + } + public sealed class TestAssetFixture() : TestAssetFixtureBase(AcceptanceFixture.NuGetGlobalPackagesFolder) { public string TargetAssetPath => GetAssetPath(AssetName); @@ -219,6 +304,19 @@ public void CategoryAAndB() { Console.WriteLine($"Running test: {nameof(CategoryAAndB)}"); } + + [TestMethod] + public void NoCategoryTest() + { + Console.WriteLine($"Running test: {nameof(NoCategoryTest)}"); + } + + [TestMethod] + [TestCategory("")] + public void EmptyCategoryTest() + { + Console.WriteLine($"Running test: {nameof(EmptyCategoryTest)}"); + } } """; } diff --git a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs new file mode 100644 index 0000000000..666dcbb9fe --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; + +using Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; + +namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel; + +[TestGroup] +public class ConditionTests +{ + [TestMethod] + public void Evaluate_EmptyStringEqualsOperator_WithNullProperty_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.Equal, string.Empty); + bool result = condition.Evaluate(propertyName => null); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_EmptyStringEqualsOperator_WithEmptyArray_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.Equal, string.Empty); + bool result = condition.Evaluate(propertyName => Array.Empty()); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_EmptyStringEqualsOperator_WithNonEmptyArray_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.Equal, string.Empty); + bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Evaluate_EmptyStringNotEqualsOperator_WithNullProperty_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); + bool result = condition.Evaluate(propertyName => null); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Evaluate_EmptyStringNotEqualsOperator_WithEmptyArray_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); + bool result = condition.Evaluate(propertyName => Array.Empty()); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Evaluate_EmptyStringNotEqualsOperator_WithNonEmptyArray_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); + bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_EmptyStringContainsOperator_WithNullProperty_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.Contains, string.Empty); + bool result = condition.Evaluate(propertyName => null); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_EmptyStringContainsOperator_WithNonEmptyArray_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.Contains, string.Empty); + bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Evaluate_EmptyStringNotContainsOperator_WithNullProperty_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.NotContains, string.Empty); + bool result = condition.Evaluate(propertyName => null); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Evaluate_EmptyStringNotContainsOperator_WithNonEmptyArray_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.NotContains, string.Empty); + bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_NonEmptyStringEqualsOperator_WithMatchingValue_ShouldReturnTrue() + { + var condition = new Condition("TestCategory", Operation.Equal, "CategoryA"); + bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); + + result.Should().BeTrue(); + } + + [TestMethod] + public void Evaluate_NonEmptyStringEqualsOperator_WithNonMatchingValue_ShouldReturnFalse() + { + var condition = new Condition("TestCategory", Operation.Equal, "CategoryA"); + bool result = condition.Evaluate(propertyName => new[] { "CategoryB" }); + + result.Should().BeFalse(); + } + + [TestMethod] + public void Parse_EmptyValueAfterEquals_ShouldCreateConditionWithEmptyValue() + { + Condition condition = Condition.Parse("TestCategory="); + + condition.Name.Should().Be("TestCategory"); + condition.Operation.Should().Be(Operation.Equal); + condition.Value.Should().Be(string.Empty); + } + + [TestMethod] + public void Parse_WhitespaceValueAfterEquals_ShouldCreateConditionWithEmptyValue() + { + Condition condition = Condition.Parse("TestCategory= "); + + condition.Name.Should().Be("TestCategory"); + condition.Operation.Should().Be(Operation.Equal); + condition.Value.Should().Be(string.Empty); + } + + [TestMethod] + public void Parse_EmptyValueWithNotEquals_ShouldCreateConditionWithEmptyValue() + { + Condition condition = Condition.Parse("TestCategory!="); + + condition.Name.Should().Be("TestCategory"); + condition.Operation.Should().Be(Operation.NotEqual); + condition.Value.Should().Be(string.Empty); + } + + [TestMethod] + public void Parse_EmptyValueWithContains_ShouldCreateConditionWithEmptyValue() + { + Condition condition = Condition.Parse("TestCategory~"); + + condition.Name.Should().Be("TestCategory"); + condition.Operation.Should().Be(Operation.Contains); + condition.Value.Should().Be(string.Empty); + } + + [TestMethod] + public void Parse_EmptyValueWithNotContains_ShouldCreateConditionWithEmptyValue() + { + Condition condition = Condition.Parse("TestCategory!~"); + + condition.Name.Should().Be("TestCategory"); + condition.Operation.Should().Be(Operation.NotContains); + condition.Value.Should().Be(string.Empty); + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/TestCategoryAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/TestCategoryAttributeTests.cs new file mode 100644 index 0000000000..e24de81409 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/TestCategoryAttributeTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests.Attributes; + +public class TestCategoryAttributeTests : TestContainer +{ + public void ConstructorShouldSetCategoryPassed() + { + var category = new TestCategoryAttribute("UnitTest"); + + category.TestCategories.Should().BeEquivalentTo(new[] { "UnitTest" }); + } + + public void ConstructorWithEmptyStringShouldResultInEmptyList() + { + var category = new TestCategoryAttribute(""); + + category.TestCategories.Should().BeEmpty(); + } + + public void ConstructorWithWhitespaceStringShouldResultInEmptyList() + { + var category = new TestCategoryAttribute(" "); + + category.TestCategories.Should().BeEmpty(); + } + + public void ConstructorWithNullShouldResultInEmptyList() + { + var category = new TestCategoryAttribute(null!); + + category.TestCategories.Should().BeEmpty(); + } + + public void ConstructorWithValidCategoryShouldPreserveValue() + { + var category = new TestCategoryAttribute("Integration"); + + category.TestCategories.Should().HaveCount(1); + category.TestCategories[0].Should().Be("Integration"); + } +} From c70bf3420a6209832cf45bd0cfefd2c90ae1b08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 31 Oct 2025 09:51:50 +0100 Subject: [PATCH 3/7] Apply suggestion from @Evangelink --- .../ObjectModel/Condition.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs index b3423c2279..1bf76e830a 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs @@ -85,6 +85,7 @@ internal bool Evaluate(Func propertyValueProvider) { result = multiValue is null or { Length: 0 }; } + // if any value in multi-valued property matches 'this.Value', for Equal to evaluate true. else if (multiValue != null) { From 03cdb90db01b39d0d5d9209f61677df3431e368b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:57:25 +0000 Subject: [PATCH 4/7] Convert ConditionTests to use MSTest assertions instead of FluentAssertions Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../ObjectModel/ConditionTests.cs | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs index 666dcbb9fe..a75454d774 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.VSTestBridge.UnitTests/ObjectModel/ConditionTests.cs @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using FluentAssertions; - using Microsoft.Testing.Extensions.VSTestBridge.ObjectModel; namespace Microsoft.Testing.Extensions.VSTestBridge.UnitTests.ObjectModel; -[TestGroup] +[TestClass] public class ConditionTests { [TestMethod] @@ -16,7 +14,7 @@ public void Evaluate_EmptyStringEqualsOperator_WithNullProperty_ShouldReturnTrue var condition = new Condition("TestCategory", Operation.Equal, string.Empty); bool result = condition.Evaluate(propertyName => null); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -25,7 +23,7 @@ public void Evaluate_EmptyStringEqualsOperator_WithEmptyArray_ShouldReturnTrue() var condition = new Condition("TestCategory", Operation.Equal, string.Empty); bool result = condition.Evaluate(propertyName => Array.Empty()); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -34,7 +32,7 @@ public void Evaluate_EmptyStringEqualsOperator_WithNonEmptyArray_ShouldReturnFal var condition = new Condition("TestCategory", Operation.Equal, string.Empty); bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -43,7 +41,7 @@ public void Evaluate_EmptyStringNotEqualsOperator_WithNullProperty_ShouldReturnF var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); bool result = condition.Evaluate(propertyName => null); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -52,7 +50,7 @@ public void Evaluate_EmptyStringNotEqualsOperator_WithEmptyArray_ShouldReturnFal var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); bool result = condition.Evaluate(propertyName => Array.Empty()); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -61,7 +59,7 @@ public void Evaluate_EmptyStringNotEqualsOperator_WithNonEmptyArray_ShouldReturn var condition = new Condition("TestCategory", Operation.NotEqual, string.Empty); bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -70,7 +68,7 @@ public void Evaluate_EmptyStringContainsOperator_WithNullProperty_ShouldReturnTr var condition = new Condition("TestCategory", Operation.Contains, string.Empty); bool result = condition.Evaluate(propertyName => null); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -79,7 +77,7 @@ public void Evaluate_EmptyStringContainsOperator_WithNonEmptyArray_ShouldReturnF var condition = new Condition("TestCategory", Operation.Contains, string.Empty); bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -88,7 +86,7 @@ public void Evaluate_EmptyStringNotContainsOperator_WithNullProperty_ShouldRetur var condition = new Condition("TestCategory", Operation.NotContains, string.Empty); bool result = condition.Evaluate(propertyName => null); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -97,7 +95,7 @@ public void Evaluate_EmptyStringNotContainsOperator_WithNonEmptyArray_ShouldRetu var condition = new Condition("TestCategory", Operation.NotContains, string.Empty); bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -106,7 +104,7 @@ public void Evaluate_NonEmptyStringEqualsOperator_WithMatchingValue_ShouldReturn var condition = new Condition("TestCategory", Operation.Equal, "CategoryA"); bool result = condition.Evaluate(propertyName => new[] { "CategoryA" }); - result.Should().BeTrue(); + Assert.IsTrue(result); } [TestMethod] @@ -115,7 +113,7 @@ public void Evaluate_NonEmptyStringEqualsOperator_WithNonMatchingValue_ShouldRet var condition = new Condition("TestCategory", Operation.Equal, "CategoryA"); bool result = condition.Evaluate(propertyName => new[] { "CategoryB" }); - result.Should().BeFalse(); + Assert.IsFalse(result); } [TestMethod] @@ -123,9 +121,9 @@ public void Parse_EmptyValueAfterEquals_ShouldCreateConditionWithEmptyValue() { Condition condition = Condition.Parse("TestCategory="); - condition.Name.Should().Be("TestCategory"); - condition.Operation.Should().Be(Operation.Equal); - condition.Value.Should().Be(string.Empty); + Assert.AreEqual("TestCategory", condition.Name); + Assert.AreEqual(Operation.Equal, condition.Operation); + Assert.AreEqual(string.Empty, condition.Value); } [TestMethod] @@ -133,9 +131,9 @@ public void Parse_WhitespaceValueAfterEquals_ShouldCreateConditionWithEmptyValue { Condition condition = Condition.Parse("TestCategory= "); - condition.Name.Should().Be("TestCategory"); - condition.Operation.Should().Be(Operation.Equal); - condition.Value.Should().Be(string.Empty); + Assert.AreEqual("TestCategory", condition.Name); + Assert.AreEqual(Operation.Equal, condition.Operation); + Assert.AreEqual(string.Empty, condition.Value); } [TestMethod] @@ -143,9 +141,9 @@ public void Parse_EmptyValueWithNotEquals_ShouldCreateConditionWithEmptyValue() { Condition condition = Condition.Parse("TestCategory!="); - condition.Name.Should().Be("TestCategory"); - condition.Operation.Should().Be(Operation.NotEqual); - condition.Value.Should().Be(string.Empty); + Assert.AreEqual("TestCategory", condition.Name); + Assert.AreEqual(Operation.NotEqual, condition.Operation); + Assert.AreEqual(string.Empty, condition.Value); } [TestMethod] @@ -153,9 +151,9 @@ public void Parse_EmptyValueWithContains_ShouldCreateConditionWithEmptyValue() { Condition condition = Condition.Parse("TestCategory~"); - condition.Name.Should().Be("TestCategory"); - condition.Operation.Should().Be(Operation.Contains); - condition.Value.Should().Be(string.Empty); + Assert.AreEqual("TestCategory", condition.Name); + Assert.AreEqual(Operation.Contains, condition.Operation); + Assert.AreEqual(string.Empty, condition.Value); } [TestMethod] @@ -163,8 +161,8 @@ public void Parse_EmptyValueWithNotContains_ShouldCreateConditionWithEmptyValue( { Condition condition = Condition.Parse("TestCategory!~"); - condition.Name.Should().Be("TestCategory"); - condition.Operation.Should().Be(Operation.NotContains); - condition.Value.Should().Be(string.Empty); + Assert.AreEqual("TestCategory", condition.Name); + Assert.AreEqual(Operation.NotContains, condition.Operation); + Assert.AreEqual(string.Empty, condition.Value); } } From 2f0c2bded4a5bd917a126bd4dc6560cb6ea3c83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 31 Oct 2025 15:32:53 +0100 Subject: [PATCH 5/7] Apply suggestion from @Evangelink --- .../ObjectModel/FastFilter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs index ec434ec2ec..c37497ae9e 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs @@ -48,7 +48,6 @@ internal bool Evaluate(Func propertyValueProvider) { // Special case: if filter contains empty string, check if property is null/empty (uncategorized) bool hasEmptyStringFilter = FilterProperties[name].Any(string.IsNullOrWhiteSpace); - // If there is no value corresponding to given name, treat it as unmatched unless filtering for empty string. if (!TryGetPropertyValue(name, propertyValueProvider, out string? singleValue, out string[]? multiValues)) { From cf36b81f948807e988faf104ae385157a6c02de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 31 Oct 2025 15:33:13 +0100 Subject: [PATCH 6/7] Apply suggestion from @Evangelink --- .../ObjectModel/FastFilter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs index c37497ae9e..1d1795cdf0 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs @@ -56,6 +56,7 @@ internal bool Evaluate(Func propertyValueProvider) matched = true; break; } + continue; } From e9dc0828ef821f26798fca477854b28074658537 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:54:06 +0000 Subject: [PATCH 7/7] Fix formatting issues: add required blank lines (SA1513, IDE2003) Co-authored-by: nohwnd <5735905+nohwnd@users.noreply.github.com> --- .../ObjectModel/Condition.cs | 2 +- .../ObjectModel/FastFilter.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs index 1bf76e830a..e178355126 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/Condition.cs @@ -85,7 +85,6 @@ internal bool Evaluate(Func propertyValueProvider) { result = multiValue is null or { Length: 0 }; } - // if any value in multi-valued property matches 'this.Value', for Equal to evaluate true. else if (multiValue != null) { @@ -135,6 +134,7 @@ internal bool Evaluate(Func propertyValueProvider) { result = multiValue is null or { Length: 0 }; } + // if any value in multi-valued property contains 'this.Value' for 'Contains' to be true. else if (multiValue != null) { diff --git a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs index 1d1795cdf0..cef2a21ecd 100644 --- a/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/FastFilter.cs @@ -48,6 +48,7 @@ internal bool Evaluate(Func propertyValueProvider) { // Special case: if filter contains empty string, check if property is null/empty (uncategorized) bool hasEmptyStringFilter = FilterProperties[name].Any(string.IsNullOrWhiteSpace); + // If there is no value corresponding to given name, treat it as unmatched unless filtering for empty string. if (!TryGetPropertyValue(name, propertyValueProvider, out string? singleValue, out string[]? multiValues)) {