Skip to content

Commit

Permalink
refactor(wip): filter, more tags; (fixme: SplitWithIgnore)
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoy312 committed May 15, 2023
1 parent a3d454f commit e4d8bb3
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 158 deletions.
45 changes: 45 additions & 0 deletions src/TestApp/shared/EngineFeatureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,50 @@ public async Task When_Test_ContentHelper()
public void When_DisplayName(string text)
{
}

[TestMethod]
[DataRow("at index 0")]
[DataRow("at index 1")]
[DataRow("at index 2")]
public void When_DataRows(string arg)
{
}

[TestMethod]
[DataRow("at index 0", "asd")]
[DataRow("at index 1", "asd")]
[DataRow("at index 2", "zxc")]
public void When_DataRows2(string arg, string arg2)
{
}

[DataTestMethod]
[DynamicData(nameof(GetDynamicData), DynamicDataSourceType.Method)]
public void When_DynamicData(string arg, string arg2)
{
}

[TestMethod]
[InjectedPointer(Windows.Devices.Input.PointerDeviceType.Touch)]
[InjectedPointer(Windows.Devices.Input.PointerDeviceType.Pen)]
public void When_InjectedPointers()
{
}

[TestMethod]
[DataRow("at index 0")]
[DataRow("at index 1")]
[InjectedPointer(Windows.Devices.Input.PointerDeviceType.Touch)]
[InjectedPointer(Windows.Devices.Input.PointerDeviceType.Pen)]
public void When_InjectedPointers_DataRows(string arg)
{
}

public static IEnumerable<object[]> GetDynamicData()
{
yield return new object[] { "at index 0", "asd" };
yield return new object[] { "at index 1", "asd" };
yield return new object[] { "at index 2", "zxc" };
}
}
}
13 changes: 5 additions & 8 deletions src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestClassInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,26 @@ namespace Uno.UI.RuntimeTests;
public class UnitTestClassInfo
{
public UnitTestClassInfo(
Type? type,
MethodInfo[]? tests,
Type type,
MethodInfo[] tests,
MethodInfo? initialize,
MethodInfo? cleanup)
{
Type = type;
TestClassName = Type?.Name ?? "(null)";
Tests = tests;
Initialize = initialize;
Cleanup = cleanup;
}

public string TestClassName { get; }
public Type Type { get; }

public Type? Type { get; }

public MethodInfo[]? Tests { get; }
public MethodInfo[] Tests { get; }

public MethodInfo? Initialize { get; }

public MethodInfo? Cleanup { get; }

public override string ToString() => TestClassName;
public override string ToString() => Type.Name;
}

#endif
41 changes: 6 additions & 35 deletions src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ namespace Uno.UI.RuntimeTests;

internal record UnitTestMethodInfo
{
private readonly List<ITestDataSource> _casesParameters;
private readonly IList<PointerDeviceType> _injectedPointerTypes;

public UnitTestMethodInfo(object testClassInstance, MethodInfo method)
{
Method = method;
Expand All @@ -30,13 +27,13 @@ public UnitTestMethodInfo(object testClassInstance, MethodInfo method)
.SingleOrDefault()
?.ExceptionType;

_casesParameters = method
DataSources = method
.GetCustomAttributes()
.Where(x => x is ITestDataSource)
.Cast<ITestDataSource>()
.ToList();

_injectedPointerTypes = method
InjectedPointerTypes = method
.GetCustomAttributes<InjectedPointerAttribute>()
.Select(attr => attr.Type)
.Distinct()
Expand All @@ -53,6 +50,10 @@ public UnitTestMethodInfo(object testClassInstance, MethodInfo method)

public bool RunsOnUIThread { get; }

public IReadOnlyList<ITestDataSource> DataSources { get; }

public IReadOnlyList<PointerDeviceType> InjectedPointerTypes { get; }

private static bool HasCustomAttribute<T>(MemberInfo? testMethod)
=> testMethod?.GetCustomAttribute(typeof(T)) != null;

Expand All @@ -73,35 +74,5 @@ public bool IsIgnored(out string ignoreMessage)
ignoreMessage = "";
return false;
}

public IEnumerable<TestCase> GetCases()
{
List<TestCase> cases = Enumerable.Empty<TestCase>().ToList();

if (_casesParameters is { Count: 0 })
{
cases.Add(new TestCase());
}

foreach (var testCaseSource in _casesParameters)
{
foreach (var caseData in testCaseSource.GetData(Method))
{
var data = testCaseSource.GetData(Method)
.SelectMany(x => x)
.ToArray();

cases.Add(new TestCase { Parameters = data, DisplayName = testCaseSource.GetDisplayName(Method, data) });
}
}

if (_injectedPointerTypes.Any())
{
var currentCases = cases;
cases = _injectedPointerTypes.SelectMany(pointer => currentCases.Select(testCase => testCase with { Pointer = pointer })).ToList();
}

return cases;
}
}
#endif
175 changes: 139 additions & 36 deletions src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.Filtering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,139 @@

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.UI.RuntimeTests.Extensions;
using Windows.Devices.Input;
using static System.StringComparison;
using KnownTags = Uno.UI.RuntimeTests.UnitTestsControl.SearchPredicate.KnownTags;

namespace Uno.UI.RuntimeTests;

partial class UnitTestsControl
{
private static IEnumerable<MethodInfo> FilterTests(UnitTestClassInfo testClassInfo, string? query)
private IEnumerable<UnitTestClassInfo> GetTestClasses(SearchPredicateCollection? filters)
{
var tests = testClassInfo.Tests?.AsEnumerable() ?? Array.Empty<MethodInfo>();
foreach (var filter in SearchPredicate.ParseQuery(query))
{
// chain filters with AND logic
tests = tests.Where(x =>
filter.Exclusion ^ // use xor to flip the result based on Exclusion
filter.Tag?.ToLowerInvariant() switch
{
"class" => MatchClassName(x, filter.Text),
"displayname" => MatchDisplayName(x, filter.Text),
"method" => MatchMethodName(x, filter.Text),
var testAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => x.GetName()?.Name?.EndsWith("Tests", OrdinalIgnoreCase) ?? false)
.Concat(new[] { GetType().GetTypeInfo().Assembly })
.Distinct();
var types = testAssemblies
.SelectMany(x => x.GetTypes())
.Where(x => filters?.MatchAll(KnownTags.ClassName, x.Name) ?? true);

_ => MatchClassName(x, filter.Text) || MatchMethodName(x, filter.Text),
}
);
if (_ciTestGroupCache != -1)
{
Console.WriteLine($"Filtering with group #{_ciTestGroupCache} (Groups {_ciTestsGroupCountCache})");
}

bool MatchClassName(MethodInfo x, string value) => x.DeclaringType?.Name.Contains(value, InvariantCultureIgnoreCase) ?? false;
bool MatchMethodName(MethodInfo x, string value) => x.Name.Contains(value, InvariantCultureIgnoreCase);
bool MatchDisplayName(MethodInfo x, string value) =>
// fixme: since we are returning MethodInfo for match, there is no way to specify
// which of the [DataRow] or which row within [DynamicData] without refactoring.
// fixme: support [DynamicData]
x.GetCustomAttributes<DataRowAttribute>().Any(y => y.DisplayName.Contains(value, InvariantCultureIgnoreCase));

return tests;
return
from type in types
where type.GetTypeInfo().GetCustomAttribute(typeof(TestClassAttribute)) != null
where _ciTestsGroupCountCache == -1 || (_ciTestsGroupCountCache != -1 && (UnitTestsControl.GetTypeTestGroup(type) % _ciTestsGroupCountCache) == _ciTestGroupCache)
orderby type.Name
let info = BuildType(type)
where info is { }
select info;
}
private static IEnumerable<MethodInfo> GetTestMethods(UnitTestClassInfo @class, SearchPredicateCollection? filters)
{
var tests = @class.Tests
.Select(x => new
{
Type = @class.Type.Name,
x.Name,
mc = filters?.MatchAll(KnownTags.Empty, @class.Type.Name),
mm = filters?.MatchAll(KnownTags.Empty, x.Name),
})
.ToArray();
return @class.Tests
.Where(x => filters is { }
? (filters.MatchAll(KnownTags.Empty, @class.Type.Name) || filters.MatchAll(KnownTags.Empty, x.Name))
: true)
.Where(x => filters?.MatchAll(KnownTags.MethodName, x.Name) ?? true)
.Where(x => filters?.MatchAll(KnownTags.FullName, $"{@class.Type.FullName}.{x.Name}") ?? true);
}

private static IEnumerable<TestCase> GetTestCases(UnitTestMethodInfo method, SearchPredicateCollection? filters)
{
return
(
from source in method.DataSources // get [DataRow, DynamicData] attributes
from data in source.GetData(method.Method) // flatten [DataRow, DynamicData] (mostly the latter)
from injectedPointerType in method.InjectedPointerTypes.Cast<PointerDeviceType?>().DefaultIfEmpty()
select new TestCase
{
Parameters = data,
DisplayName = source.GetDisplayName(method.Method, data)
}
)
// convert empty (without [DataRow, DynamicData]) to a single null case, so we dont lose them
.DefaultIfEmpty()//.Select(x => x ?? new TestCase())
.Select((x, i) => new
{
Case = x,
Index = i,
IsDataSourced = x is not null,
Parameters = x?.Parameters.Any() == true ? string.Join(",", x.Parameters) : null,
})
.Where(x => filters?.MatchAllOnlyIfValuePresent(KnownTags.Parameters, x.Parameters) ?? true)
.Where(x => filters?.MatchAllOnlyIf(KnownTags.AtIndex, x.IsDataSourced, y => MatchIndex(y, x.Index)) ?? true)
.Select(x => x.Case ?? new());

bool MatchIndex(SearchPredicate predicate, int index)
{
return predicate.Parts.Any(x => x.Raw == index.ToString());
}
}

public record SearchPredicate(string Raw, string Text, bool Exclusion = false, string? Tag = null, params SearchPredicatePart[] Parts)
{
public static class KnownTags
{
public const string? Empty = null;
public const string ClassName = "class";
public const string MethodName = "method";
public const string FullName = "full_name";
public const string DisplayName = "display_name";
public const string AtIndex = "at";
public const string Parameters = "params";
}

public static SearchPredicateComparer DefaultComparer = new();
public static SearchQueryComparer DefaultQueryComparer = new();

// fixme: domain specific configuration should be injectable
private static readonly IReadOnlyDictionary<string, string> NamespaceAliases =
private static readonly IReadOnlyDictionary<string, string> NamespaceAliases = // aliases for tag
new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
["c"] = "class",
["m"] = "method",
["d"] = "display_name",
["c"] = KnownTags.ClassName,
["m"] = KnownTags.MethodName,
["f"] = KnownTags.FullName,
["d"] = KnownTags.DisplayName,
["p"] = KnownTags.Parameters,
};
private static readonly IReadOnlyDictionary<string, string> SpecialPrefixAliases = // prefixes are tags that don't need ':'
new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
["@"] = KnownTags.AtIndex,
};

public static SearchPredicate[] ParseQuery(string? query)
public static SearchPredicateCollection? ParseQuery(string? query)
{
if (string.IsNullOrWhiteSpace(query)) return Array.Empty<SearchPredicate>();
if (string.IsNullOrWhiteSpace(query)) return null;

return query!.SplitWithIgnore(' ', @""".*?(?<!\\)""", skipEmptyEntries: true)
return new SearchPredicateCollection(query
!.SplitWithIgnore(' ', @""".*?(?<!\\)""", skipEmptyEntries: true)
.Select(ParseFragment)
.OfType<SearchPredicate>() // trim null
.Where(x => x.Text.Length > 0) // ignore empty tag query eg: "c:"
.ToArray();
.ToArray()
);
}

public static SearchPredicate? ParseFragment(string criteria)
Expand All @@ -84,8 +150,15 @@ public static SearchPredicate[] ParseQuery(string? query)
text = text.Substring(1);
}
var tag = default(string?);
if (text.Split(':', 2) is { Length: 2 } tagParts)
if (SpecialPrefixAliases.FirstOrDefault(x => text.StartsWith(x.Key)) is { Key.Length: > 0 } prefix)
{
// process prefixes (tags that dont need ':'): '@0' -> 'at:0'
tag = prefix.Value;
text = text.Substring(prefix.Key.Length);
}
else if (text.Split(':', 2) is { Length: 2 } tagParts)
{
// process tag aliases
tag = NamespaceAliases.TryGetValue(tagParts[0], out var value) ? value : tagParts[0];
text = tagParts[1];
}
Expand All @@ -96,8 +169,9 @@ public static SearchPredicate[] ParseQuery(string? query)
return new(raw, text, exclusion, tag, parts);
}

public bool IsMatch(string input) => Parts
.Any(x => (x.MatchStart, x.MatchEnd) switch
public bool IsMatch(string input) =>
Exclusion ^ // use xor to flip the result based on Exclusion
Parts.Any(x => (x.MatchStart, x.MatchEnd) switch
{
(true, false) => input.StartsWith(x.Text),
(false, true) => input.EndsWith(x.Text),
Expand Down Expand Up @@ -166,6 +240,35 @@ public static SearchPredicatePart Parse(string part)
return new(raw, text, matchStart, matchEnd);
}
}
public class SearchPredicateCollection : ReadOnlyCollection<SearchPredicate>
{
public SearchPredicateCollection(IList<SearchPredicate> list) : base(list) { }

/// <summary>
/// Match value against all filters of specified tag.
/// </summary>
/// <param name="tag"></param>
/// <param name="value"></param>
/// <returns>If all 'tag' filters matches the value, or true if the none filters are of 'tag'.</returns>
public bool MatchAll(string? tag, string value) => this
.Where(x => x.Tag == tag)
.All(x => x.IsMatch(value));

public bool MatchAllOnlyIfValuePresent(string? tag, string? value) =>
MatchAllOnlyIf(tag, value is not null, x => x.IsMatch(value!));

public bool MatchAllOnlyIf(string? tag, bool condition, Func<SearchPredicate, bool> match)
{
var tagFilters = this.Where(x => x.Tag == tag).ToArray();

return (condition, tagFilters.Any()) switch
{
(true, true) => tagFilters.All(x => match(x)),
(false, true) => false,
(_, false) => true,
};
}
}
}

#endif
Loading

0 comments on commit e4d8bb3

Please sign in to comment.