Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend filter functionality: support filtering by category and filtering by text #1162

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/TestModel/model/Filter/CategoryFilter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Filters the TestNodes by test categories. Use item 'No category' to filter for tests without any test category.
/// </summary>
public class CategoryFilter : ITestFilter
{
public const string NoCategory = "No category";

private List<string> _condition = new List<string>();

internal CategoryFilter(ITestModel model)
{
TestModel = model;
}

private ITestModel TestModel { get; }

public string FilterId => "CategoryFilter";

public IEnumerable<string> Condition
{
get { return _condition; }
set { _condition = value.ToList(); }
}

public IEnumerable<string> 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<string> categories = new List<string>();
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<string> GetAllCategories()
{
var items = TestModel.AvailableCategories;
var allCategories = items.Concat(new[] { NoCategory });
return allCategories.ToList();
}
}
}
45 changes: 45 additions & 0 deletions src/TestModel/model/Filter/ITestCentricTestFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// ***********************************************************************
// 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
{
/// <summary>
/// Provides filter functionality: by outcome, by duration, by category...
/// </summary>
public interface ITestCentricTestFilter
{
/// <summary>
/// Filters the loaded TestNodes by outcome (for example: 'Passed', 'Failed' or 'Not run')
/// </summary>
IEnumerable<string> OutcomeFilter { get; set; }

/// <summary>
/// Filters the loaded TestNodes by matching a text (for example: Namespace, Class name or test method name - filter is case insensitive)
/// </summary>
string TextFilter { get; set; }

/// <summary>
/// Filters the loaded TestNodes by test categories. Use item 'No category' to filter for tests without any test category.
/// </summary>
IEnumerable<string> CategoryFilter { get; set; }

/// <summary>
/// Returns the list of available test categories defined in the loaded TestNodes + item 'No category'
/// </summary>
IEnumerable<string> AllCategories { get; }

/// <summary>
/// Clear all actives filters and reset them to default
/// </summary>
void ClearAllFilters();

/// <summary>
/// Init filter after a project is loaded
/// </summary>
void Init();
}
}
40 changes: 40 additions & 0 deletions src/TestModel/model/Filter/ITestFilter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Interface for all test filters
/// </summary>
internal interface ITestFilter
{
/// <summary>
/// Unqiue identifier of the filter
/// </summary>
string FilterId { get; }

/// <summary>
/// The filter condition
/// </summary>
IEnumerable<string> Condition { get; set; }

/// <summary>
/// Reset the filter condition to its default state
/// </summary>
void Reset();

/// <summary>
/// Init filter after a project is loaded
/// </summary>
void Init();

/// <summary>
/// Checks if the testNode matches the filter condition
/// </summary>
bool IsMatching(TestNode testNode);
}
}
73 changes: 73 additions & 0 deletions src/TestModel/model/Filter/OutcomeFilter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Filters the TestNodes by outcome (for example: 'Passed', 'Failed' or 'Not run')
/// </summary>
public class OutcomeFilter : ITestFilter
{
public const string AllOutcome = "All";
public const string NotRunOutcome = "Not Run";

private List<string> _condition = new List<string>() { AllOutcome };

internal OutcomeFilter(ITestModel model)
{
TestModel = model;
}

public string FilterId => "OutcomeFilter";

private ITestModel TestModel { get; }

public IEnumerable<string> 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<string>() { AllOutcome };
}

public void Init()
{
_condition = new List<string>() { AllOutcome };
}
}
}
110 changes: 110 additions & 0 deletions src/TestModel/model/Filter/TestCentricTestFilter.cs
Original file line number Diff line number Diff line change
@@ -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<ITestFilter> _filters = new List<ITestFilter>();

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<string> OutcomeFilter
{
get => GetFilterCondition("OutcomeFilter");
set => SetFilterCondition("OutcomeFilter", value);
}

public IEnumerable<string> CategoryFilter
{
get => GetFilterCondition("CategoryFilter");

set => SetFilterCondition("CategoryFilter", value);
}

public string TextFilter
{
get => GetFilterCondition("TextFilter").First();
set => SetFilterCondition("TextFilter", new string[] { value });
}

public IEnumerable<string> AllCategories
{
get
{
var categoryFilter = _filters.FirstOrDefault(f => f.FilterId == "CategoryFilter") as CategoryFilter;
return categoryFilter?.AllCategories ?? Enumerable.Empty<string>();
}
}

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<string> GetFilterCondition(string filterId)
{
var testFilter = _filters.FirstOrDefault(f => f.FilterId == filterId);
return testFilter.Condition ?? Enumerable.Empty<string>();
}

private void SetFilterCondition(string filterId, IEnumerable<string> 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();
}
}
}
47 changes: 47 additions & 0 deletions src/TestModel/model/Filter/TextFilter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Filters the TestNodes by matching a text (for example: Namespace, Class name or test method name - filter is case insensitive)
/// </summary>
internal class TextFilter : ITestFilter
{
private string _condition = string.Empty;

public string FilterId => "TextFilter";

public IEnumerable<string> Condition
{
get { return new List<string>() { _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;
}
}
}
Loading