From d6d05daf9e8cf01762da7bb5f3fabe8842e6f3bb Mon Sep 17 00:00:00 2001
From: rowo360 <59574371+rowo360@users.noreply.github.com>
Date: Thu, 5 Dec 2024 06:12:22 +0100
Subject: [PATCH 1/2] Extend filter functionality: support filtering by
category and filtering by text
---
src/TestModel/model/ITestCentricTestFilter.cs | 27 +-
src/TestModel/model/TestCentricTestFilter.cs | 116 +++-
.../tests/TestCentricTestFilterTests.cs | 506 ++++++++++++++++++
3 files changed, 633 insertions(+), 16 deletions(-)
create mode 100644 src/TestModel/tests/TestCentricTestFilterTests.cs
diff --git a/src/TestModel/model/ITestCentricTestFilter.cs b/src/TestModel/model/ITestCentricTestFilter.cs
index 7f193461..d3efd5c4 100644
--- a/src/TestModel/model/ITestCentricTestFilter.cs
+++ b/src/TestModel/model/ITestCentricTestFilter.cs
@@ -13,8 +13,33 @@ namespace TestCentric.Gui.Model
public interface ITestCentricTestFilter
{
///
- /// Filters the loaded TestNodes by outcome
+ /// Filters the loaded TestNodes by outcome (for example: 'Passed', 'Failed' or 'Not run')
///
IEnumerable OutcomeFilter { get; set; }
+
+ ///
+ /// Filters the loaded TestNodes by matching a text (for example: Namespace, Class name or test method name - filter is case insensitive)
+ ///
+ string TextFilter { get; set; }
+
+ ///
+ /// Filters the loaded TestNodes by test categories. Use item 'No category' to filter for tests without any test category.
+ ///
+ IEnumerable CategoryFilter { get; set; }
+
+ ///
+ /// Returns the list of available test categories defined in the loaded TestNodes + item 'No category'
+ ///
+ IEnumerable AllCategories { get; }
+
+ ///
+ /// Clear all actives filters and reset them to default
+ ///
+ void ClearAllFilters();
+
+ ///
+ /// Init filter after a project is loaded
+ ///
+ void Init();
}
}
diff --git a/src/TestModel/model/TestCentricTestFilter.cs b/src/TestModel/model/TestCentricTestFilter.cs
index 76cd0afa..d893dcd4 100644
--- a/src/TestModel/model/TestCentricTestFilter.cs
+++ b/src/TestModel/model/TestCentricTestFilter.cs
@@ -6,23 +6,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Xml;
namespace TestCentric.Gui.Model
{
- internal class TestCentricTestFilter : ITestCentricTestFilter
+ public class TestCentricTestFilter : ITestCentricTestFilter
{
+ public const string AllOutcome = "All";
+ public const string NotRunOutcome = "Not Run";
+ public const string NoCategory = "No category";
+
+
// By default: all outcome filters are enabled
- List _outcomeFilter = new List()
- {
- "Passed",
- "Failed",
- "Ignored",
- "Skipped",
- "Inconclusive",
- "Not Run",
- };
-
- internal TestCentricTestFilter(TestModel model, Action filterChangedEvent)
+ private List _outcomeFilter = new List() { AllOutcome };
+ private string _textFilter = string.Empty;
+ private List _categoryFilter = new List();
+
+ public TestCentricTestFilter(ITestModel model, Action filterChangedEvent)
{
TestModel = model;
FireFilterChangedEvent = filterChangedEvent;
@@ -44,6 +44,48 @@ public IEnumerable OutcomeFilter
}
}
+ public IEnumerable CategoryFilter
+ {
+ get => _categoryFilter;
+
+ set
+ {
+ _categoryFilter = value.ToList();
+ FilterNodes(TestModel.LoadedTests);
+ FireFilterChangedEvent();
+ }
+ }
+
+ public string TextFilter
+ {
+ get { return _textFilter; }
+ set
+ {
+ _textFilter = value;
+ FilterNodes(TestModel.LoadedTests);
+ FireFilterChangedEvent();
+ }
+ }
+
+ public IEnumerable AllCategories { get; private set; }
+
+ public void Init()
+ {
+ AllCategories = GetAllCategories();
+ CategoryFilter = AllCategories;
+ }
+
+ public void ClearAllFilters()
+ {
+ _outcomeFilter = new List() { AllOutcome };
+ _categoryFilter = GetAllCategories();
+ _textFilter = string.Empty;
+
+ FilterNodes(TestModel.LoadedTests);
+ FireFilterChangedEvent();
+ }
+
+
private bool FilterNodes(TestNode testNode)
{
// 1. Check if any child is visible => parent must be visible too
@@ -53,14 +95,51 @@ private bool FilterNodes(TestNode testNode)
childIsVisible = true;
// 2. Check if node itself is visible
- bool isVisible = IsOutcomeFilterMatching(testNode);
+ bool isVisible = IsOutcomeFilterMatching(testNode) && IsTextFilterMatching(testNode) && IsCategoryMatching(testNode);
testNode.IsVisible = isVisible || childIsVisible;
return testNode.IsVisible;
}
+ private bool IsTextFilterMatching(TestNode testNode)
+ {
+ if (string.IsNullOrEmpty(_textFilter))
+ {
+ return true;
+ }
+
+ return testNode.FullName.IndexOf(_textFilter, StringComparison.InvariantCultureIgnoreCase) > -1;
+ }
+
+ private bool IsCategoryMatching(TestNode testNode)
+ {
+ if (CategoryFilter.Any() == false)
+ return false;
+
+ string xpathExpression = "ancestor-or-self::*/properties/property[@name='Category']";
+
+ // 1. Get list of available categories at TestNode
+ IList categories = new List();
+ foreach (XmlNode node in testNode.Xml.SelectNodes(xpathExpression))
+ {
+ var groupName = node.Attributes["value"].Value;
+ if (!string.IsNullOrEmpty(groupName))
+ categories.Add(groupName);
+ }
+
+ if (categories.Any() == false)
+ categories.Add(NoCategory);
+
+ // 2. Check if any filter category matches the available categories
+ return CategoryFilter.Intersect(categories).Any();
+ }
+
private bool IsOutcomeFilterMatching(TestNode testNode)
{
- string outcome = "Not Run";
+ // All kind of outcomes should be displayed (no outcome filtering)
+ if (OutcomeFilter.Contains(AllOutcome))
+ return true;
+
+ string outcome = NotRunOutcome;
var result = TestModel.GetResultForTest(testNode.Id);
if (result != null)
@@ -73,12 +152,19 @@ private bool IsOutcomeFilterMatching(TestNode testNode)
outcome = result.Outcome.Status.ToString();
break;
case TestStatus.Skipped:
- outcome = result.Outcome.Label == "Ignored" ? "Ignored" : "Skippeed";
+ outcome = result.Outcome.Label == "Ignored" ? "Ignored" : "Skipped";
break;
}
}
return OutcomeFilter.Contains(outcome);
}
+
+ private List GetAllCategories()
+ {
+ var items = TestModel.AvailableCategories;
+ var allCategories = items.Concat(new[] { NoCategory });
+ return allCategories.ToList();
+ }
}
}
diff --git a/src/TestModel/tests/TestCentricTestFilterTests.cs b/src/TestModel/tests/TestCentricTestFilterTests.cs
new file mode 100644
index 00000000..9ff6be29
--- /dev/null
+++ b/src/TestModel/tests/TestCentricTestFilterTests.cs
@@ -0,0 +1,506 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NSubstitute;
+using NUnit.Framework;
+using NUnit.Framework.Internal;
+
+namespace TestCentric.Gui.Model
+{
+ [TestFixture]
+ internal class TestCentricTestFilterTests
+ {
+ private ITestModel _model;
+
+ [SetUp]
+ public void Setup()
+ {
+ _model = Substitute.For();
+ }
+
+ [Test]
+ public void Set_OutcomeFilter_FilterChangedEvent_IsInvoked()
+ {
+ // Arrange
+ bool isCalled = false;
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { isCalled = true; });
+ testFilter.OutcomeFilter = new List();
+
+ // Assert
+ Assert.That(isCalled, Is.True);
+ }
+
+ [Test]
+ public void Set_TextFilter_FilterChangedEvent_IsInvoked()
+ {
+ // Arrange
+ bool isCalled = false;
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { isCalled = true; });
+ testFilter.TextFilter = "";
+
+ // Assert
+ Assert.That(isCalled, Is.True);
+ }
+
+ [Test]
+ public void Set_CategoryFilter_FilterChangedEvent_IsInvoked()
+ {
+ // Arrange
+ bool isCalled = false;
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { isCalled = true; });
+ testFilter.CategoryFilter = new List();
+
+ // Assert
+ Assert.That(isCalled, Is.True);
+ }
+
+ [Test]
+ public void AllCategories_NoCategoriesDefinedInModel_ReturnsDefaultCategory()
+ {
+ // Arrange
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ var allCategories = testFilter.AllCategories;
+
+ // Assert
+ Assert.That(allCategories.Count(), Is.EqualTo(1));
+ Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+ }
+
+ [Test]
+ public void AllCategories_CategoriesDefinedInModel_ReturnsModelAndDefaultCategory()
+ {
+ // Arrange
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+ _model.AvailableCategories.Returns(new List() { "Feature_1" });
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ var allCategories = testFilter.AllCategories;
+
+ // Assert
+ Assert.That(allCategories.Count(), Is.EqualTo(2));
+ Assert.That(allCategories, Contains.Item("Feature_1"));
+ Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+ }
+
+ [Test]
+ public void ClearFilter_AllFiltersAreReset()
+ {
+ // Arrange
+ var testNode = new TestNode($"");
+ _model.LoadedTests.Returns(testNode);
+ _model.AvailableCategories.Returns(new List() { "Feature_1" });
+
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.TextFilter = "TestA";
+ testFilter.CategoryFilter = new List() { "Feature_1" };
+ testFilter.OutcomeFilter = new List() { "Passed" };
+
+ // Act
+ testFilter.ClearAllFilters();
+
+ // Assert
+ var allCategories = testFilter.AllCategories;
+ Assert.That(allCategories.Count(), Is.EqualTo(2));
+ Assert.That(allCategories, Contains.Item("Feature_1"));
+ Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+
+ var outcomeFilter = testFilter.OutcomeFilter;
+ Assert.That(outcomeFilter.Count, Is.EqualTo(1));
+ Assert.That(outcomeFilter, Contains.Item(TestCentricTestFilter.AllOutcome));
+
+ Assert.That(testFilter.TextFilter, Is.Empty);
+ }
+
+ private static object[] FilterByOutcomeTestCases =
+ {
+ new object[] { new List() { "Passed" }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022" } },
+ new object[] { new List() { "Failed" }, new List() { "3-1000", "3-1001", "3-1020", "3-1021" } },
+ new object[] { new List() { TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032" } },
+ new object[] { new List() { "Passed", TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022", "3-1030", "3-1031", "3-1032" } },
+ new object[] { new List() { "Passed", "Failed" }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022", "3-1020", "3-1021" } },
+ new object[] { new List() { TestCentricTestFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032", "3-1020", "3-1021" } },
+ new object[] { new List() { TestCentricTestFilter.AllOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1021", "3-1022", "3-1030", "3-1031", "3-1032" } },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByOutcomeTestCases))]
+ public void FilterByOutcome_TestNodesAreVisible(IList outcomeFilter, IList expectedVisibleNodes)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1001", "NamespaceA", "Failed",
+ CreateTestFixtureXml("3-1010", "Fixture_1", "Passed",
+ CreateTestcaseXml("3-1011", "TestA", "Passed"),
+ CreateTestcaseXml("3-1012", "TestB", "Passed")) +
+ CreateTestFixtureXml("3-1020", "Fixture_2", "Failed",
+ CreateTestcaseXml("3-1021", "TestA", "Failed"),
+ CreateTestcaseXml("3-1022", "TestB", "Passed")) +
+ CreateTestFixtureXml("3-1030", "Fixture_3", "",
+ CreateTestcaseXml("3-1031", "TestA", ""),
+ CreateTestcaseXml("3-1032", "TestB", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.OutcomeFilter = outcomeFilter;
+
+ // Assert
+ foreach (string testId in expectedVisibleNodes)
+ {
+ TestNode node = GetTestNode(testNode, testId);
+ Assert.That(node.IsVisible, Is.True);
+ }
+ }
+
+ private static object[] FilterByOutcomeNamespacesTestCases =
+{
+ new object[] { new List() { "Passed" }, new List() { "3-1000", "3-1000", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1212" } },
+ new object[] { new List() { "Failed" }, new List() { "3-1000", "3-1200", "3-1210", "3-1211", "3-1400", "3-1411" } },
+ new object[] { new List() { TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1300", "3-1310", "3-1311", "3-1400", "3-1410", "3-1412" } },
+ new object[] { new List() { "Passed", TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1412" } },
+ new object[] { new List() { "Passed", "Failed" }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1400", "3-1411" } },
+ new object[] { new List() { TestCentricTestFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1200", "3-1210", "3-1211", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
+ new object[] { new List() { TestCentricTestFilter.AllOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByOutcomeNamespacesTestCases))]
+ public void FilterByOutcome_Namespaces_TestNodesAreVisible(IList outcomeFilter, IList expectedVisibleNodes)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1100", "NamespaceA", "Passed",
+ CreateTestFixtureXml("3-1110", "Fixture_1", "Passed",
+ CreateTestcaseXml("3-1111", "TestA", "Passed"),
+ CreateTestcaseXml("3-1112", "TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1200", "NamespaceB", "Failed",
+ CreateTestFixtureXml("3-1210", "Fixture_2", "Failed",
+ CreateTestcaseXml("3-1211", "TestA", "Failed"),
+ CreateTestcaseXml("3-1212", "TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1300", "NamespaceC", "",
+ CreateTestFixtureXml("3-1310", "Fixture_1", "",
+ CreateTestcaseXml("3-1311", "TestA", ""),
+ CreateTestcaseXml("3-1312", "TestB", ""))) +
+ CreateTestSuiteXml("3-1400", "NamespaceD", "Failed",
+ CreateTestFixtureXml("3-1410", "Fixture_2", "",
+ CreateTestcaseXml("3-1411", "TestC", "Failed"),
+ CreateTestcaseXml("3-1412", "TestD", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.OutcomeFilter = outcomeFilter;
+
+ // Assert
+ foreach (string testId in expectedVisibleNodes)
+ {
+ TestNode node = GetTestNode(testNode, testId);
+ Assert.That(node.IsVisible, Is.True);
+ }
+ }
+
+ private static object[] FilterByTextTestCases =
+ {
+ new object[] { "NamespaceA", new List() { "3-1000", "3-1000", "3-1110", "3-1111", "3-1112" } },
+ new object[] { "TestA", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1200", "3-1210", "3-1211", "3-1300", "3-1310", "3-1311" } },
+ new object[] { "TestC", new List() { "3-1000", "3-1300", "3-1320", "3-1321"} },
+ new object[] { "_1", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1300", "3-1310", "3-1311", "3-1312", } },
+ new object[] { "aryA", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1320", "3-1321", "3-1322" } },
+ new object[] { "Namespace", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1320", "3-1321", "3-1322" } },
+ new object[] { "Fixture", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1320", "3-1321", "3-1322" } },
+ new object[] { "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1320", "3-1321", "3-1322" } },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByTextTestCases))]
+ public void FilterByText_TestNodesAreVisible(string textFilter, IList expectedVisibleNodes)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1100", "LibraryA.NamespaceA", "Passed",
+ CreateTestFixtureXml("3-1110", "LibraryA.NamespaceA.Fixture_1", "Passed",
+ CreateTestcaseXml("3-1111", "LibraryA.NamespaceA.Fixture_1.TestA", "Passed"),
+ CreateTestcaseXml("3-1112", "LibraryA.NamespaceA.Fixture_1.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1200", "LibraryA.NamespaceB", "Failed",
+ CreateTestFixtureXml("3-1210", "LibraryA.NamespaceB.Fixture_2", "Failed",
+ CreateTestcaseXml("3-1211", "LibraryA.NamespaceB.Fixture_2.TestA", "Failed"),
+ CreateTestcaseXml("3-1212", "LibraryA.NamespaceB.Fixture_2.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1300", "LibraryA.NamespaceC", "",
+ CreateTestFixtureXml("3-1310", "LibraryA.NamespaceC.Fixture_1", "",
+ CreateTestcaseXml("3-1311", "LibraryA.NamespaceC.Fixture_1.TestA", ""),
+ CreateTestcaseXml("3-1312", "LibraryA.NamespaceC.Fixture_1.TestB", "")) +
+ CreateTestFixtureXml("3-1320", "LibraryA.NamespaceC.Fixture_2", "",
+ CreateTestcaseXml("3-1321", "LibraryA.NamespaceC.Fixture_2.TestC", "Failed"),
+ CreateTestcaseXml("3-1322", "LibraryA.NamespaceC.Fixture_2.TestD", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.TextFilter = textFilter;
+
+ // Assert
+ foreach (string testId in expectedVisibleNodes)
+ {
+ TestNode node = GetTestNode(testNode, testId);
+ Assert.That(node.IsVisible, Is.True);
+ }
+ }
+
+ private static object[] FilterByCategoryTestCases =
+ {
+ new object[] { new[] { "Category_1" }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1300", "3-1310", "3-1311", "3-1312" } },
+ new object[] { new[] { "Category_1", "Category_2" }, new List() { "3-1100", "3-1000", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1311", "3-1312" } },
+ new object[] { new[] { "Category_2" }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1312" } },
+ new object[] { new[] { "Category_3" }, new List() { "3-1000", "3-1300", "3-1320", "3-1321" } },
+ new object[] { new[] { "Category_3", TestCentricTestFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1321", "3-1322" } },
+ new object[] { new[] { TestCentricTestFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1322" } },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByCategoryTestCases))]
+ public void FilterByCategory_TestNodesAreVisible(IList categoryFilter, IList expectedVisibleNodes)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1100", "LibraryA.NamespaceA", "Passed",
+ CreateTestFixtureXml("3-1110", "LibraryA.NamespaceA.Fixture_1", "Passed", new[] { "Category_1", "Category_2" },
+ CreateTestcaseXml("3-1111", "LibraryA.NamespaceA.Fixture_1.TestA", "Passed"),
+ CreateTestcaseXml("3-1112", "LibraryA.NamespaceA.Fixture_1.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1200", "LibraryA.NamespaceB", "Failed",
+ CreateTestFixtureXml("3-1210", "LibraryA.NamespaceB.Fixture_2", "Failed",
+ CreateTestcaseXml("3-1211", "LibraryA.NamespaceB.Fixture_2.TestA", "Failed", new[] { "Category_2" }),
+ CreateTestcaseXml("3-1212", "LibraryA.NamespaceB.Fixture_2.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1300", "LibraryA.NamespaceC", "",
+ CreateTestFixtureXml("3-1310", "LibraryA.NamespaceC.Fixture_1", "", new[] { "Category_1" },
+ CreateTestcaseXml("3-1311", "LibraryA.NamespaceC.Fixture_1.TestA", ""),
+ CreateTestcaseXml("3-1312", "LibraryA.NamespaceC.Fixture_1.TestB", "", new[] { "Category_2" })) +
+ CreateTestFixtureXml("3-1320", "LibraryA.NamespaceC.Fixture_2", "",
+ CreateTestcaseXml("3-1321", "LibraryA.NamespaceC.Fixture_2.TestC", "Failed", new[] { "Category_3" }),
+ CreateTestcaseXml("3-1322", "LibraryA.NamespaceC.Fixture_2.TestD", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.CategoryFilter= categoryFilter;
+
+ // Assert
+ foreach (string testId in expectedVisibleNodes)
+ {
+ TestNode node = GetTestNode(testNode, testId);
+ Assert.That(node.IsVisible, Is.True);
+ }
+ }
+
+ private static object[] FilterByCategoryCategoryAndTextTestCases =
+ {
+ new object[] { new[] { "Category_1" }, new[] { "Passed" }, "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112" } },
+ new object[] { new[] { "Category_2" }, new[] { "Failed" }, "", new List() { "3-1000", "3-1200", "3-1211" } },
+ new object[] { new[] { "Category_2" }, new[] { TestCentricTestFilter.NotRunOutcome }, "", new List() { "3-1000", "3-1300", "3-1312" } },
+ new object[] { new[] { "Category_2" }, new[] { TestCentricTestFilter.AllOutcome }, "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1312" } },
+ new object[] { new[] { "Category_2" }, new[] { "Passed", "Failed" }, "TestB", new List() { "3-1000", "3-1100", "3-1110", "3-1112" } },
+ new object[] { new[] { "Category_1" }, new[] { TestCentricTestFilter.AllOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1311", "3-1312" } },
+ new object[] { new[] { "Category_1", "Category_2"}, new[] { "Failed" }, "TestA", new List() { "3-1000", "3-1200", "3-1210", "3-1211" } },
+ new object[] { new[] { "Category_3" }, new[] { "Failed" }, "TestC", new List() { "3-1000", "3-1300", "3-1320", "3-1321" } },
+ new object[] { new[] { TestCentricTestFilter.NoCategory }, new[] { TestCentricTestFilter.NotRunOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1320", "3-1322" } },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByCategoryCategoryAndTextTestCases))]
+ public void FilterByOutcomeCategoryAndText_TestNodesAreVisible(IList categoryFilter, IList outcomeFilter, string textFilter, IList expectedVisibleNodes)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1100", "LibraryA.NamespaceA", "Passed",
+ CreateTestFixtureXml("3-1110", "LibraryA.NamespaceA.Fixture_1", "Passed", new[] { "Category_1", "Category_2" },
+ CreateTestcaseXml("3-1111", "LibraryA.NamespaceA.Fixture_1.TestA", "Passed"),
+ CreateTestcaseXml("3-1112", "LibraryA.NamespaceA.Fixture_1.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1200", "LibraryA.NamespaceB", "Failed",
+ CreateTestFixtureXml("3-1210", "LibraryA.NamespaceB.Fixture_2", "Failed",
+ CreateTestcaseXml("3-1211", "LibraryA.NamespaceB.Fixture_2.TestA", "Failed", new[] { "Category_2" }),
+ CreateTestcaseXml("3-1212", "LibraryA.NamespaceB.Fixture_2.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1300", "LibraryA.NamespaceC", "",
+ CreateTestFixtureXml("3-1310", "LibraryA.NamespaceC.Fixture_1", "", new[] { "Category_1" },
+ CreateTestcaseXml("3-1311", "LibraryA.NamespaceC.Fixture_1.TestA", ""),
+ CreateTestcaseXml("3-1312", "LibraryA.NamespaceC.Fixture_1.TestB", "", new[] { "Category_2" })) +
+ CreateTestFixtureXml("3-1320", "LibraryA.NamespaceC.Fixture_2", "",
+ CreateTestcaseXml("3-1321", "LibraryA.NamespaceC.Fixture_2.TestC", "Failed", new[] { "Category_3" }),
+ CreateTestcaseXml("3-1322", "LibraryA.NamespaceC.Fixture_2.TestD", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.CategoryFilter = categoryFilter;
+ testFilter.OutcomeFilter = outcomeFilter;
+ testFilter.TextFilter = textFilter;
+
+ // Assert
+ foreach (string testId in expectedVisibleNodes)
+ {
+ TestNode node = GetTestNode(testNode, testId);
+ Assert.That(node.IsVisible, Is.True);
+ }
+ }
+
+ private static object[] FilterByCategoryCategoryAndTextAllInvisibleTestCases =
+{
+ new object[] { new[] { "Category_XY" }, new[] { TestCentricTestFilter.AllOutcome }, ""},
+ new object[] { new[] { "Category_1" }, new[] { "Passed", "Failed" }, "NamespaceXY"},
+ new object[] { new[] { "Category_3" }, new[] { TestCentricTestFilter.NotRunOutcome }, ""},
+ new object[] { new[] { TestCentricTestFilter.NoCategory }, new[] { TestCentricTestFilter.NotRunOutcome }, "TestC"},
+ new object[] { new[] { "Category_2" }, new[] { "Failed" }, "TestB"},
+ };
+
+ [Test]
+ [TestCaseSource(nameof(FilterByCategoryCategoryAndTextAllInvisibleTestCases))]
+ public void FilterByOutcomeCategoryAndText_AllNodesAreInvisible(IList categoryFilter, IList outcomeFilter, string textFilter)
+ {
+ // Arrange
+ TestNode testNode = new TestNode(
+ CreateTestSuiteXml("3-1000", "LibraryA", "Failed",
+ CreateTestSuiteXml("3-1100", "LibraryA.NamespaceA", "Passed",
+ CreateTestFixtureXml("3-1110", "LibraryA.NamespaceA.Fixture_1", "Passed", new[] { "Category_1", "Category_2" },
+ CreateTestcaseXml("3-1111", "LibraryA.NamespaceA.Fixture_1.TestA", "Passed"),
+ CreateTestcaseXml("3-1112", "LibraryA.NamespaceA.Fixture_1.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1200", "LibraryA.NamespaceB", "Failed",
+ CreateTestFixtureXml("3-1210", "LibraryA.NamespaceB.Fixture_2", "Failed",
+ CreateTestcaseXml("3-1211", "LibraryA.NamespaceB.Fixture_2.TestA", "Failed", new[] { "Category_2" }),
+ CreateTestcaseXml("3-1212", "LibraryA.NamespaceB.Fixture_2.TestB", "Passed"))) +
+ CreateTestSuiteXml("3-1300", "LibraryA.NamespaceC", "",
+ CreateTestFixtureXml("3-1310", "LibraryA.NamespaceC.Fixture_1", "", new[] { "Category_1" },
+ CreateTestcaseXml("3-1311", "LibraryA.NamespaceC.Fixture_1.TestA", ""),
+ CreateTestcaseXml("3-1312", "LibraryA.NamespaceC.Fixture_1.TestB", "", new[] { "Category_2" })) +
+ CreateTestFixtureXml("3-1320", "LibraryA.NamespaceC.Fixture_2", "",
+ CreateTestcaseXml("3-1321", "LibraryA.NamespaceC.Fixture_2.TestC", "Failed", new[] { "Category_3" }),
+ CreateTestcaseXml("3-1322", "LibraryA.NamespaceC.Fixture_2.TestD", "")))));
+ _model.LoadedTests.Returns(testNode);
+
+ // Act
+ TestCentricTestFilter testFilter = new TestCentricTestFilter(_model, () => { });
+ testFilter.Init();
+ testFilter.CategoryFilter = categoryFilter;
+ testFilter.OutcomeFilter = outcomeFilter;
+ testFilter.TextFilter = textFilter;
+
+ // Assert
+ AssertTestNodeIsInvisible(testNode);
+ }
+
+ private void AssertTestNodeIsInvisible(TestNode testNode)
+ {
+ Assert.That(testNode.IsVisible, Is.False, $"TestNode {testNode.Id} is not invisible.");
+ foreach (TestNode child in testNode.Children)
+ AssertTestNodeIsInvisible(child);
+ }
+
+ private TestNode GetTestNode(TestNode testNode, string testId)
+ {
+ if (testNode.Id == testId)
+ return testNode;
+
+ foreach (TestNode child in testNode.Children)
+ {
+ TestNode n = GetTestNode(child, testId);
+ if (n != null)
+ return n;
+ }
+
+ return null;
+ }
+
+ private string CreateTestcaseXml(string testId, string testName, string outcome)
+ {
+ return CreateTestcaseXml(testId, testName, outcome, new List());
+ }
+
+ private string CreateTestcaseXml(string testId, string testName, string outcome, IList categories)
+ {
+ string str = $" ";
+
+ str += " ";
+ foreach (string category in categories)
+ str += $" ";
+ str += " ";
+
+ str += " ";
+
+ if (!string.IsNullOrEmpty(outcome))
+ _model.GetResultForTest(testId).Returns(new ResultNode($""));
+ return str;
+ }
+
+ private string CreateTestFixtureXml(string testId, string testName, string outcome, params string[] testCases)
+ {
+ return CreateTestFixtureXml(testId, testName, outcome, new List(), testCases);
+ }
+
+ private string CreateTestFixtureXml(string testId, string testName, string outcome, IEnumerable categories, params string[] testCases)
+ {
+ string str = $" ";
+
+ str += " ";
+ foreach (string category in categories)
+ str += $" ";
+ str += " ";
+
+ foreach (string testCase in testCases)
+ str += testCase;
+
+ str += "";
+
+ if (!string.IsNullOrEmpty(outcome))
+ _model.GetResultForTest(testId).Returns(new ResultNode($""));
+
+ return str;
+ }
+
+ private string CreateTestSuiteXml(string testId, string testName, string outcome, params string[] testSuites)
+ {
+ string str = $" ";
+ foreach (string testSuite in testSuites)
+ str += testSuite;
+
+ str += "";
+
+ if (!string.IsNullOrEmpty(outcome))
+ _model.GetResultForTest(testId).Returns(new ResultNode($""));
+
+ return str;
+ }
+ }
+}
From 9e061a093a0afd5bc0579416ff2a7f7878ed59ca Mon Sep 17 00:00:00 2001
From: rowo360 <59574371+rowo360@users.noreply.github.com>
Date: Fri, 6 Dec 2024 08:32:09 +0100
Subject: [PATCH 2/2] Refactor TestCentricTestFilter class: introduce separate
TestFilter classes, each one dedicated to one single filter condition
---
src/TestModel/model/Filter/CategoryFilter.cs | 79 ++++++++
.../{ => Filter}/ITestCentricTestFilter.cs | 2 +-
src/TestModel/model/Filter/ITestFilter.cs | 40 +++++
src/TestModel/model/Filter/OutcomeFilter.cs | 73 ++++++++
.../model/Filter/TestCentricTestFilter.cs | 110 ++++++++++++
src/TestModel/model/Filter/TextFilter.cs | 47 +++++
src/TestModel/model/ITestModel.cs | 1 +
src/TestModel/model/TestCentricTestFilter.cs | 170 ------------------
src/TestModel/model/TestModel.cs | 1 +
.../tests/TestCentricTestFilterTests.cs | 43 ++---
10 files changed, 374 insertions(+), 192 deletions(-)
create mode 100644 src/TestModel/model/Filter/CategoryFilter.cs
rename src/TestModel/model/{ => Filter}/ITestCentricTestFilter.cs (97%)
create mode 100644 src/TestModel/model/Filter/ITestFilter.cs
create mode 100644 src/TestModel/model/Filter/OutcomeFilter.cs
create mode 100644 src/TestModel/model/Filter/TestCentricTestFilter.cs
create mode 100644 src/TestModel/model/Filter/TextFilter.cs
delete mode 100644 src/TestModel/model/TestCentricTestFilter.cs
diff --git a/src/TestModel/model/Filter/CategoryFilter.cs b/src/TestModel/model/Filter/CategoryFilter.cs
new file mode 100644
index 00000000..52e2ee99
--- /dev/null
+++ b/src/TestModel/model/Filter/CategoryFilter.cs
@@ -0,0 +1,79 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+
+namespace TestCentric.Gui.Model.Filter
+{
+ ///
+ /// Filters the TestNodes by test categories. Use item 'No category' to filter for tests without any test category.
+ ///
+ public class CategoryFilter : ITestFilter
+ {
+ public const string NoCategory = "No category";
+
+ private List _condition = new List();
+
+ internal CategoryFilter(ITestModel model)
+ {
+ TestModel = model;
+ }
+
+ private ITestModel TestModel { get; }
+
+ public string FilterId => "CategoryFilter";
+
+ public IEnumerable Condition
+ {
+ get { return _condition; }
+ set { _condition = value.ToList(); }
+ }
+
+ public IEnumerable AllCategories { get; private set; }
+
+ public bool IsMatching(TestNode testNode)
+ {
+ if (_condition.Any() == false)
+ return false;
+
+ string xpathExpression = "ancestor-or-self::*/properties/property[@name='Category']";
+
+ // 1. Get list of available categories at TestNode
+ IList categories = new List();
+ foreach (XmlNode node in testNode.Xml.SelectNodes(xpathExpression))
+ {
+ var groupName = node.Attributes["value"].Value;
+ if (!string.IsNullOrEmpty(groupName))
+ categories.Add(groupName);
+ }
+
+ if (categories.Any() == false)
+ categories.Add(NoCategory);
+
+ // 2. Check if any filter category matches the available categories
+ return _condition.Intersect(categories).Any();
+ }
+
+ public void Reset()
+ {
+ _condition = GetAllCategories();
+ }
+
+ public void Init()
+ {
+ AllCategories = GetAllCategories();
+ _condition = AllCategories.ToList();
+ }
+
+ private List GetAllCategories()
+ {
+ var items = TestModel.AvailableCategories;
+ var allCategories = items.Concat(new[] { NoCategory });
+ return allCategories.ToList();
+ }
+ }
+}
diff --git a/src/TestModel/model/ITestCentricTestFilter.cs b/src/TestModel/model/Filter/ITestCentricTestFilter.cs
similarity index 97%
rename from src/TestModel/model/ITestCentricTestFilter.cs
rename to src/TestModel/model/Filter/ITestCentricTestFilter.cs
index d3efd5c4..fd635f2a 100644
--- a/src/TestModel/model/ITestCentricTestFilter.cs
+++ b/src/TestModel/model/Filter/ITestCentricTestFilter.cs
@@ -5,7 +5,7 @@
using System.Collections.Generic;
-namespace TestCentric.Gui.Model
+namespace TestCentric.Gui.Model.Filter
{
///
/// Provides filter functionality: by outcome, by duration, by category...
diff --git a/src/TestModel/model/Filter/ITestFilter.cs b/src/TestModel/model/Filter/ITestFilter.cs
new file mode 100644
index 00000000..98981036
--- /dev/null
+++ b/src/TestModel/model/Filter/ITestFilter.cs
@@ -0,0 +1,40 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System.Collections.Generic;
+
+namespace TestCentric.Gui.Model.Filter
+{
+ ///
+ /// Interface for all test filters
+ ///
+ internal interface ITestFilter
+ {
+ ///
+ /// Unqiue identifier of the filter
+ ///
+ string FilterId { get; }
+
+ ///
+ /// The filter condition
+ ///
+ IEnumerable Condition { get; set; }
+
+ ///
+ /// Reset the filter condition to its default state
+ ///
+ void Reset();
+
+ ///
+ /// Init filter after a project is loaded
+ ///
+ void Init();
+
+ ///
+ /// Checks if the testNode matches the filter condition
+ ///
+ bool IsMatching(TestNode testNode);
+ }
+}
diff --git a/src/TestModel/model/Filter/OutcomeFilter.cs b/src/TestModel/model/Filter/OutcomeFilter.cs
new file mode 100644
index 00000000..a27eee7f
--- /dev/null
+++ b/src/TestModel/model/Filter/OutcomeFilter.cs
@@ -0,0 +1,73 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TestCentric.Gui.Model.Filter
+{
+ ///
+ /// Filters the TestNodes by outcome (for example: 'Passed', 'Failed' or 'Not run')
+ ///
+ public class OutcomeFilter : ITestFilter
+ {
+ public const string AllOutcome = "All";
+ public const string NotRunOutcome = "Not Run";
+
+ private List _condition = new List() { AllOutcome };
+
+ internal OutcomeFilter(ITestModel model)
+ {
+ TestModel = model;
+ }
+
+ public string FilterId => "OutcomeFilter";
+
+ private ITestModel TestModel { get; }
+
+ public IEnumerable Condition
+ {
+ get { return _condition; }
+ set { _condition = value.ToList(); }
+ }
+
+ public bool IsMatching(TestNode testNode)
+ {
+ // All kind of outcomes should be displayed (no outcome filtering)
+ if (_condition.Contains(AllOutcome))
+ return true;
+
+ string outcome = NotRunOutcome;
+
+ var result = TestModel.GetResultForTest(testNode.Id);
+ if (result != null)
+ {
+ switch (result.Outcome.Status)
+ {
+ case TestStatus.Failed:
+ case TestStatus.Passed:
+ case TestStatus.Inconclusive:
+ outcome = result.Outcome.Status.ToString();
+ break;
+ case TestStatus.Skipped:
+ outcome = result.Outcome.Label == "Ignored" ? "Ignored" : "Skipped";
+ break;
+ }
+ }
+
+ return _condition.Contains(outcome);
+ }
+
+ public void Reset()
+ {
+ _condition = new List() { AllOutcome };
+ }
+
+ public void Init()
+ {
+ _condition = new List() { AllOutcome };
+ }
+ }
+}
diff --git a/src/TestModel/model/Filter/TestCentricTestFilter.cs b/src/TestModel/model/Filter/TestCentricTestFilter.cs
new file mode 100644
index 00000000..b1b7b5f0
--- /dev/null
+++ b/src/TestModel/model/Filter/TestCentricTestFilter.cs
@@ -0,0 +1,110 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TestCentric.Gui.Model.Filter
+{
+ public class TestCentricTestFilter : ITestCentricTestFilter
+ {
+ private List _filters = new List();
+
+ public TestCentricTestFilter(ITestModel model, Action filterChangedEvent)
+ {
+ TestModel = model;
+ FireFilterChangedEvent = filterChangedEvent;
+
+ _filters.Add(new OutcomeFilter(model));
+ _filters.Add(new TextFilter());
+ _filters.Add(new CategoryFilter(model));
+ }
+
+ private ITestModel TestModel { get; }
+
+ private Action FireFilterChangedEvent;
+
+ public IEnumerable OutcomeFilter
+ {
+ get => GetFilterCondition("OutcomeFilter");
+ set => SetFilterCondition("OutcomeFilter", value);
+ }
+
+ public IEnumerable CategoryFilter
+ {
+ get => GetFilterCondition("CategoryFilter");
+
+ set => SetFilterCondition("CategoryFilter", value);
+ }
+
+ public string TextFilter
+ {
+ get => GetFilterCondition("TextFilter").First();
+ set => SetFilterCondition("TextFilter", new string[] { value });
+ }
+
+ public IEnumerable AllCategories
+ {
+ get
+ {
+ var categoryFilter = _filters.FirstOrDefault(f => f.FilterId == "CategoryFilter") as CategoryFilter;
+ return categoryFilter?.AllCategories ?? Enumerable.Empty();
+ }
+ }
+
+ public void ClearAllFilters()
+ {
+ foreach (ITestFilter filter in _filters)
+ {
+ filter.Reset();
+ }
+
+ FilterNodes(TestModel.LoadedTests);
+ FireFilterChangedEvent();
+ }
+
+ public void Init()
+ {
+ foreach (ITestFilter filter in _filters)
+ {
+ filter.Init();
+ }
+ }
+
+ private bool FilterNodes(TestNode testNode)
+ {
+ // 1. Check if any child is visible => parent must be visible too
+ bool childIsVisible = false;
+ foreach (TestNode child in testNode.Children)
+ if (FilterNodes(child))
+ childIsVisible = true;
+
+ // 2. Check if node itself is visible
+ bool isVisible = _filters.All(f => f.IsMatching(testNode));
+ testNode.IsVisible = isVisible || childIsVisible;
+ return testNode.IsVisible;
+ }
+
+ private IEnumerable GetFilterCondition(string filterId)
+ {
+ var testFilter = _filters.FirstOrDefault(f => f.FilterId == filterId);
+ return testFilter.Condition ?? Enumerable.Empty();
+ }
+
+ private void SetFilterCondition(string filterId, IEnumerable filter)
+ {
+ // 1. Get concrete filter by ID
+ var testFilter = _filters.FirstOrDefault(f => f.FilterId == filterId);
+ if (testFilter == null)
+ return;
+
+ // 2. Set condition, apply new filter to all nodes and fire event
+ testFilter.Condition = filter;
+ FilterNodes(TestModel.LoadedTests);
+ FireFilterChangedEvent();
+ }
+ }
+}
diff --git a/src/TestModel/model/Filter/TextFilter.cs b/src/TestModel/model/Filter/TextFilter.cs
new file mode 100644
index 00000000..217beec6
--- /dev/null
+++ b/src/TestModel/model/Filter/TextFilter.cs
@@ -0,0 +1,47 @@
+// ***********************************************************************
+// Copyright (c) Charlie Poole and TestCentric contributors.
+// Licensed under the MIT License. See LICENSE file in root directory.
+// ***********************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TestCentric.Gui.Model.Filter
+{
+ ///
+ /// Filters the TestNodes by matching a text (for example: Namespace, Class name or test method name - filter is case insensitive)
+ ///
+ internal class TextFilter : ITestFilter
+ {
+ private string _condition = string.Empty;
+
+ public string FilterId => "TextFilter";
+
+ public IEnumerable Condition
+ {
+ get { return new List() { _condition }; }
+ set { _condition = value.FirstOrDefault(); }
+ }
+
+ public bool IsMatching(TestNode testNode)
+ {
+ if (string.IsNullOrEmpty(_condition))
+ {
+ return true;
+ }
+
+ return testNode.FullName.IndexOf(_condition, StringComparison.InvariantCultureIgnoreCase) > -1;
+ }
+
+ public void Reset()
+ {
+ _condition = string.Empty;
+ }
+
+ public void Init()
+ {
+ _condition = string.Empty;
+ }
+ }
+}
diff --git a/src/TestModel/model/ITestModel.cs b/src/TestModel/model/ITestModel.cs
index b4fa22a8..886132c8 100644
--- a/src/TestModel/model/ITestModel.cs
+++ b/src/TestModel/model/ITestModel.cs
@@ -11,6 +11,7 @@ namespace TestCentric.Gui.Model
using TestCentric.Engine;
using Services;
using Settings;
+ using TestCentric.Gui.Model.Filter;
public interface ITestModel : IDisposable
{
diff --git a/src/TestModel/model/TestCentricTestFilter.cs b/src/TestModel/model/TestCentricTestFilter.cs
deleted file mode 100644
index d893dcd4..00000000
--- a/src/TestModel/model/TestCentricTestFilter.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-// ***********************************************************************
-// Copyright (c) Charlie Poole and TestCentric contributors.
-// Licensed under the MIT License. See LICENSE file in root directory.
-// ***********************************************************************
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml;
-
-namespace TestCentric.Gui.Model
-{
- public class TestCentricTestFilter : ITestCentricTestFilter
- {
- public const string AllOutcome = "All";
- public const string NotRunOutcome = "Not Run";
- public const string NoCategory = "No category";
-
-
- // By default: all outcome filters are enabled
- private List _outcomeFilter = new List() { AllOutcome };
- private string _textFilter = string.Empty;
- private List _categoryFilter = new List();
-
- public TestCentricTestFilter(ITestModel model, Action filterChangedEvent)
- {
- TestModel = model;
- FireFilterChangedEvent = filterChangedEvent;
- }
-
- private ITestModel TestModel { get; }
-
- private Action FireFilterChangedEvent;
-
- public IEnumerable OutcomeFilter
- {
- get => _outcomeFilter;
-
- set
- {
- _outcomeFilter = value.ToList();
- FilterNodes(TestModel.LoadedTests);
- FireFilterChangedEvent();
- }
- }
-
- public IEnumerable CategoryFilter
- {
- get => _categoryFilter;
-
- set
- {
- _categoryFilter = value.ToList();
- FilterNodes(TestModel.LoadedTests);
- FireFilterChangedEvent();
- }
- }
-
- public string TextFilter
- {
- get { return _textFilter; }
- set
- {
- _textFilter = value;
- FilterNodes(TestModel.LoadedTests);
- FireFilterChangedEvent();
- }
- }
-
- public IEnumerable AllCategories { get; private set; }
-
- public void Init()
- {
- AllCategories = GetAllCategories();
- CategoryFilter = AllCategories;
- }
-
- public void ClearAllFilters()
- {
- _outcomeFilter = new List() { AllOutcome };
- _categoryFilter = GetAllCategories();
- _textFilter = string.Empty;
-
- FilterNodes(TestModel.LoadedTests);
- FireFilterChangedEvent();
- }
-
-
- private bool FilterNodes(TestNode testNode)
- {
- // 1. Check if any child is visible => parent must be visible too
- bool childIsVisible = false;
- foreach (TestNode child in testNode.Children)
- if (FilterNodes(child))
- childIsVisible = true;
-
- // 2. Check if node itself is visible
- bool isVisible = IsOutcomeFilterMatching(testNode) && IsTextFilterMatching(testNode) && IsCategoryMatching(testNode);
- testNode.IsVisible = isVisible || childIsVisible;
- return testNode.IsVisible;
- }
-
- private bool IsTextFilterMatching(TestNode testNode)
- {
- if (string.IsNullOrEmpty(_textFilter))
- {
- return true;
- }
-
- return testNode.FullName.IndexOf(_textFilter, StringComparison.InvariantCultureIgnoreCase) > -1;
- }
-
- private bool IsCategoryMatching(TestNode testNode)
- {
- if (CategoryFilter.Any() == false)
- return false;
-
- string xpathExpression = "ancestor-or-self::*/properties/property[@name='Category']";
-
- // 1. Get list of available categories at TestNode
- IList categories = new List();
- foreach (XmlNode node in testNode.Xml.SelectNodes(xpathExpression))
- {
- var groupName = node.Attributes["value"].Value;
- if (!string.IsNullOrEmpty(groupName))
- categories.Add(groupName);
- }
-
- if (categories.Any() == false)
- categories.Add(NoCategory);
-
- // 2. Check if any filter category matches the available categories
- return CategoryFilter.Intersect(categories).Any();
- }
-
- private bool IsOutcomeFilterMatching(TestNode testNode)
- {
- // All kind of outcomes should be displayed (no outcome filtering)
- if (OutcomeFilter.Contains(AllOutcome))
- return true;
-
- string outcome = NotRunOutcome;
-
- var result = TestModel.GetResultForTest(testNode.Id);
- if (result != null)
- {
- switch (result.Outcome.Status)
- {
- case TestStatus.Failed:
- case TestStatus.Passed:
- case TestStatus.Inconclusive:
- outcome = result.Outcome.Status.ToString();
- break;
- case TestStatus.Skipped:
- outcome = result.Outcome.Label == "Ignored" ? "Ignored" : "Skipped";
- break;
- }
- }
-
- return OutcomeFilter.Contains(outcome);
- }
-
- private List GetAllCategories()
- {
- var items = TestModel.AvailableCategories;
- var allCategories = items.Concat(new[] { NoCategory });
- return allCategories.ToList();
- }
- }
-}
diff --git a/src/TestModel/model/TestModel.cs b/src/TestModel/model/TestModel.cs
index e8b9a6b5..a1afbd4a 100644
--- a/src/TestModel/model/TestModel.cs
+++ b/src/TestModel/model/TestModel.cs
@@ -18,6 +18,7 @@ namespace TestCentric.Gui.Model
using System.Runtime.InteropServices;
using Services;
using Settings;
+ using TestCentric.Gui.Model.Filter;
public class TestModel : ITestModel
{
diff --git a/src/TestModel/tests/TestCentricTestFilterTests.cs b/src/TestModel/tests/TestCentricTestFilterTests.cs
index 9ff6be29..f44db2ca 100644
--- a/src/TestModel/tests/TestCentricTestFilterTests.cs
+++ b/src/TestModel/tests/TestCentricTestFilterTests.cs
@@ -9,6 +9,7 @@
using NSubstitute;
using NUnit.Framework;
using NUnit.Framework.Internal;
+using TestCentric.Gui.Model.Filter;
namespace TestCentric.Gui.Model
{
@@ -85,7 +86,7 @@ public void AllCategories_NoCategoriesDefinedInModel_ReturnsDefaultCategory()
// Assert
Assert.That(allCategories.Count(), Is.EqualTo(1));
- Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+ Assert.That(allCategories, Contains.Item(CategoryFilter.NoCategory));
}
[Test]
@@ -104,7 +105,7 @@ public void AllCategories_CategoriesDefinedInModel_ReturnsModelAndDefaultCategor
// Assert
Assert.That(allCategories.Count(), Is.EqualTo(2));
Assert.That(allCategories, Contains.Item("Feature_1"));
- Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+ Assert.That(allCategories, Contains.Item(CategoryFilter.NoCategory));
}
[Test]
@@ -128,11 +129,11 @@ public void ClearFilter_AllFiltersAreReset()
var allCategories = testFilter.AllCategories;
Assert.That(allCategories.Count(), Is.EqualTo(2));
Assert.That(allCategories, Contains.Item("Feature_1"));
- Assert.That(allCategories, Contains.Item(TestCentricTestFilter.NoCategory));
+ Assert.That(allCategories, Contains.Item(CategoryFilter.NoCategory));
var outcomeFilter = testFilter.OutcomeFilter;
Assert.That(outcomeFilter.Count, Is.EqualTo(1));
- Assert.That(outcomeFilter, Contains.Item(TestCentricTestFilter.AllOutcome));
+ Assert.That(outcomeFilter, Contains.Item(OutcomeFilter.AllOutcome));
Assert.That(testFilter.TextFilter, Is.Empty);
}
@@ -141,11 +142,11 @@ public void ClearFilter_AllFiltersAreReset()
{
new object[] { new List() { "Passed" }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022" } },
new object[] { new List() { "Failed" }, new List() { "3-1000", "3-1001", "3-1020", "3-1021" } },
- new object[] { new List() { TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032" } },
- new object[] { new List() { "Passed", TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022", "3-1030", "3-1031", "3-1032" } },
+ new object[] { new List() { OutcomeFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032" } },
+ new object[] { new List() { "Passed", OutcomeFilter.NotRunOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022", "3-1030", "3-1031", "3-1032" } },
new object[] { new List() { "Passed", "Failed" }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1022", "3-1020", "3-1021" } },
- new object[] { new List() { TestCentricTestFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032", "3-1020", "3-1021" } },
- new object[] { new List() { TestCentricTestFilter.AllOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1021", "3-1022", "3-1030", "3-1031", "3-1032" } },
+ new object[] { new List() { OutcomeFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1001", "3-1030", "3-1031", "3-1032", "3-1020", "3-1021" } },
+ new object[] { new List() { OutcomeFilter.AllOutcome }, new List() { "3-1000", "3-1001", "3-1010", "3-1011", "3-1012", "3-1020", "3-1021", "3-1022", "3-1030", "3-1031", "3-1032" } },
};
[Test]
@@ -184,11 +185,11 @@ public void FilterByOutcome_TestNodesAreVisible(IList outcomeFilter, ILi
{
new object[] { new List() { "Passed" }, new List() { "3-1000", "3-1000", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1212" } },
new object[] { new List() { "Failed" }, new List() { "3-1000", "3-1200", "3-1210", "3-1211", "3-1400", "3-1411" } },
- new object[] { new List() { TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1300", "3-1310", "3-1311", "3-1400", "3-1410", "3-1412" } },
- new object[] { new List() { "Passed", TestCentricTestFilter.NotRunOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1412" } },
+ new object[] { new List() { OutcomeFilter.NotRunOutcome }, new List() { "3-1000", "3-1300", "3-1310", "3-1311", "3-1400", "3-1410", "3-1412" } },
+ new object[] { new List() { "Passed", OutcomeFilter.NotRunOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1412" } },
new object[] { new List() { "Passed", "Failed" }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1400", "3-1411" } },
- new object[] { new List() { TestCentricTestFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1200", "3-1210", "3-1211", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
- new object[] { new List() { TestCentricTestFilter.AllOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
+ new object[] { new List() { OutcomeFilter.NotRunOutcome, "Failed" }, new List() { "3-1000", "3-1200", "3-1210", "3-1211", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
+ new object[] { new List() { OutcomeFilter.AllOutcome }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1210", "3-1211", "3-1212", "3-1300", "3-1310", "3-1311", "3-1312", "3-1400", "3-1410", "3-1411", "3-1412" } },
};
[Test]
@@ -284,8 +285,8 @@ public void FilterByText_TestNodesAreVisible(string textFilter, IList ex
new object[] { new[] { "Category_1", "Category_2" }, new List() { "3-1100", "3-1000", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1311", "3-1312" } },
new object[] { new[] { "Category_2" }, new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1312" } },
new object[] { new[] { "Category_3" }, new List() { "3-1000", "3-1300", "3-1320", "3-1321" } },
- new object[] { new[] { "Category_3", TestCentricTestFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1321", "3-1322" } },
- new object[] { new[] { TestCentricTestFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1322" } },
+ new object[] { new[] { "Category_3", CategoryFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1321", "3-1322" } },
+ new object[] { new[] { CategoryFilter.NoCategory }, new List() { "3-1000", "3-1200", "3-1210", "3-1212", "3-1300", "3-1320", "3-1322" } },
};
[Test]
@@ -329,13 +330,13 @@ public void FilterByCategory_TestNodesAreVisible(IList categoryFilter, I
{
new object[] { new[] { "Category_1" }, new[] { "Passed" }, "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112" } },
new object[] { new[] { "Category_2" }, new[] { "Failed" }, "", new List() { "3-1000", "3-1200", "3-1211" } },
- new object[] { new[] { "Category_2" }, new[] { TestCentricTestFilter.NotRunOutcome }, "", new List() { "3-1000", "3-1300", "3-1312" } },
- new object[] { new[] { "Category_2" }, new[] { TestCentricTestFilter.AllOutcome }, "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1312" } },
+ new object[] { new[] { "Category_2" }, new[] { OutcomeFilter.NotRunOutcome }, "", new List() { "3-1000", "3-1300", "3-1312" } },
+ new object[] { new[] { "Category_2" }, new[] { OutcomeFilter.AllOutcome }, "", new List() { "3-1000", "3-1100", "3-1110", "3-1111", "3-1112", "3-1200", "3-1211", "3-1300", "3-1312" } },
new object[] { new[] { "Category_2" }, new[] { "Passed", "Failed" }, "TestB", new List() { "3-1000", "3-1100", "3-1110", "3-1112" } },
- new object[] { new[] { "Category_1" }, new[] { TestCentricTestFilter.AllOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1311", "3-1312" } },
+ new object[] { new[] { "Category_1" }, new[] { OutcomeFilter.AllOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1311", "3-1312" } },
new object[] { new[] { "Category_1", "Category_2"}, new[] { "Failed" }, "TestA", new List() { "3-1000", "3-1200", "3-1210", "3-1211" } },
new object[] { new[] { "Category_3" }, new[] { "Failed" }, "TestC", new List() { "3-1000", "3-1300", "3-1320", "3-1321" } },
- new object[] { new[] { TestCentricTestFilter.NoCategory }, new[] { TestCentricTestFilter.NotRunOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1320", "3-1322" } },
+ new object[] { new[] { CategoryFilter.NoCategory }, new[] { OutcomeFilter.NotRunOutcome }, "NamespaceC", new List() { "3-1000", "3-1300", "3-1320", "3-1322" } },
};
[Test]
@@ -379,10 +380,10 @@ public void FilterByOutcomeCategoryAndText_TestNodesAreVisible(IList cat
private static object[] FilterByCategoryCategoryAndTextAllInvisibleTestCases =
{
- new object[] { new[] { "Category_XY" }, new[] { TestCentricTestFilter.AllOutcome }, ""},
+ new object[] { new[] { "Category_XY" }, new[] { OutcomeFilter.AllOutcome }, ""},
new object[] { new[] { "Category_1" }, new[] { "Passed", "Failed" }, "NamespaceXY"},
- new object[] { new[] { "Category_3" }, new[] { TestCentricTestFilter.NotRunOutcome }, ""},
- new object[] { new[] { TestCentricTestFilter.NoCategory }, new[] { TestCentricTestFilter.NotRunOutcome }, "TestC"},
+ new object[] { new[] { "Category_3" }, new[] { OutcomeFilter.NotRunOutcome }, ""},
+ new object[] { new[] { CategoryFilter.NoCategory }, new[] { OutcomeFilter.NotRunOutcome }, "TestC"},
new object[] { new[] { "Category_2" }, new[] { "Failed" }, "TestB"},
};