Skip to content

Commit

Permalink
feat: improved query syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoy312 committed May 14, 2023
1 parent cbf16c3 commit a3d454f
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 42 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,22 @@ and define the following in your `csproj`:
```
These attributes will ask for the runtime test engine to replace the ones defined by the `Uno.UI.RuntimeTests.Engine` package.

## Test runner (UnitTestsControl) filtering syntax
- Search terms are separated by space. Multiple consecutive spaces are treated same as one.
- Multiple search terms are chained with AND logic.
- Search terms are case insensitive.
- `-` can be used before any term for exclusion, effectively inverting the results.
- Special tags can be used to match certain part of the test: // syntax: tag:term
- `class` or `c` matches the class name
- `method` or `m` matches the method name
- `displayname` or `d` matches the display name in [DataRow]
- Search term without a prefixing tag will match either of method name or class name.

Examples:
- `listview`
- `listview measure`
- `listview measure -recycle`
- `c:listview m:measure -m:recycle`

## Running the tests automatically during CI
_TBD_
41 changes: 41 additions & 0 deletions src/TestApp/shared/EngineFeatureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

#if HAS_UNO_WINUI || WINDOWS_WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#else
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#endif

namespace Uno.UI.RuntimeTests.Engine
{
/// <summary>
/// Contains tests relevant to the RTT engine features.
/// </summary>
[TestClass]
public class MetaTests
{
[TestMethod]
[RunsOnUIThread]
public async Task When_Test_ContentHelper()
{
var SUT = new TextBlock() { Text = "Hello" };
UnitTestsUIContentHelper.Content = SUT;

await UnitTestsUIContentHelper.WaitForIdle();
await UnitTestsUIContentHelper.WaitForLoaded(SUT);
}

[TestMethod]
[DataRow("hello", DisplayName = "hello test")]
[DataRow("goodbye", DisplayName = "goodbye test")]
public void When_DisplayName(string text)
{
}
}
}
21 changes: 3 additions & 18 deletions src/TestApp/shared/SanityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

namespace Uno.UI.RuntimeTests.Engine
{
/// <summary>
/// Contains sanity/smoke tests used to assert basic scenarios.
/// </summary>
[TestClass]
public class SanityTests
{
Expand All @@ -28,24 +31,6 @@ public async Task Is_Still_Sane()
await Task.Delay(2000);
}

[TestMethod]
[RunsOnUIThread]
public async Task When_Test_ContentHelper()
{
var SUT = new TextBlock() { Text = "Hello" };
UnitTestsUIContentHelper.Content = SUT;

await UnitTestsUIContentHelper.WaitForIdle();
await UnitTestsUIContentHelper.WaitForLoaded(SUT);
}

[TestMethod]
[DataRow("hello", DisplayName = "hello test")]
[DataRow("goodbye", DisplayName = "goodbye test")]
public void Is_Sane_With_Cases(string text)
{
}

#if DEBUG
[TestMethod]
public async Task No_Longer_Sane() // expected to fail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)MetaAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EngineFeatureTests.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="$(MSBuildThisFileDirectory)MainPage.xaml">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Uno.UI.RuntimeTests.Extensions;

internal static partial class StringExtensions
{
/// <summary>
/// Like <see cref="string.Split(char[])"/>, but allows exception to be made with a Regex pattern.
/// </summary>
/// <param name="input"></param>
/// <param name="separator"></param>
/// <param name="ignoredPattern">segments matched by the regex will not be splited.</param>
/// <param name="skipEmptyEntries"></param>
/// <returns></returns>
public static string[] SplitWithIgnore(this string input, char separator, string ignoredPattern, bool skipEmptyEntries)
{
var ignores = Regex.Matches(input, ignoredPattern);

var shards = new List<string>();
for (int i = 0; i < input.Length; i++)
{
var nextSpaceDelimiter = input.IndexOf(separator, i);

// find the next space, if inside a quote
while (nextSpaceDelimiter != -1 && ignores.FirstOrDefault(x => InRange(x, nextSpaceDelimiter)) is { } enclosingIgnore)
{
nextSpaceDelimiter = enclosingIgnore.Index + enclosingIgnore.Length + 1 is { } afterIgnore && afterIgnore < input.Length
? input.IndexOf(separator, afterIgnore)
: -1;
}

if (nextSpaceDelimiter != -1)
{
shards.Add(input.Substring(i, nextSpaceDelimiter - i));
i = nextSpaceDelimiter;

// skip multiple continous spaces
while (skipEmptyEntries && i + 1 < input.Length && input[i + 1] == separator) i++;
}
else
{
shards.Add(input.Substring(i));
break;
}
}

return shards.ToArray();

bool InRange(Match x, int index) => x.Index <= index && index < (x.Index + x.Length);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class UnitTestEngineConfig

public static UnitTestEngineConfig Default { get; } = new UnitTestEngineConfig();

public string[]? Filters { get; set; }
public string? Query { get; set; }

public int Attempts { get; set; } = DefaultRepeatCount;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#if !UNO_RUNTIMETESTS_DISABLE_UI

#nullable enable

using System;
using System.Collections.Generic;
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 static System.StringComparison;

namespace Uno.UI.RuntimeTests;

partial class UnitTestsControl
{
private static IEnumerable<MethodInfo> FilterTests(UnitTestClassInfo testClassInfo, string? query)
{
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),

_ => MatchClassName(x, filter.Text) || MatchMethodName(x, filter.Text),
}
);
}

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


public record SearchPredicate(string Raw, string Text, bool Exclusion = false, string? Tag = null, params SearchPredicatePart[] Parts)
{
public static SearchPredicateComparer DefaultComparer = new();
public static SearchQueryComparer DefaultQueryComparer = new();

// fixme: domain specific configuration should be injectable
private static readonly IReadOnlyDictionary<string, string> NamespaceAliases =
new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
["c"] = "class",
["m"] = "method",
["d"] = "display_name",
};

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

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

public static SearchPredicate? ParseFragment(string criteria)
{
if (string.IsNullOrWhiteSpace(criteria)) return null;

var raw = criteria.Trim();
var text = raw;
if (text.StartsWith('-') is var exclusion && exclusion)
{
text = text.Substring(1);
}
var tag = default(string?);
if (text.Split(':', 2) is { Length: 2 } tagParts)
{
tag = NamespaceAliases.TryGetValue(tagParts[0], out var value) ? value : tagParts[0];
text = tagParts[1];
}
var parts = text.SplitWithIgnore(',', @""".*?(?<!\\)""", skipEmptyEntries: false)
.Select(SearchPredicatePart.Parse)
.ToArray();

return new(raw, text, exclusion, tag, parts);
}

public bool IsMatch(string input) => Parts
.Any(x => (x.MatchStart, x.MatchEnd) switch
{
(true, false) => input.StartsWith(x.Text),
(false, true) => input.EndsWith(x.Text),

_ => input.Contains(x.Text),
});

public class SearchPredicateComparer : IEqualityComparer<SearchPredicate?>
{
public int GetHashCode(SearchPredicate? obj) => obj?.GetHashCode() ?? -1;
public bool Equals(SearchPredicate? x, SearchPredicate? y)
{
return (x, y) switch
{
(null, null) => true,
(null, _) => false,
(_, null) => false,
_ =>
x.Raw == y.Raw &&
x.Text == y.Text &&
x.Exclusion == y.Exclusion &&
x.Tag == y.Tag &&
x.Parts.SequenceEqual(y.Parts),
};
}
}

public class SearchQueryComparer : IEqualityComparer<SearchPredicate[]?>
{
public int GetHashCode(SearchPredicate[]? obj) => obj?.GetHashCode() ?? -1;
public bool Equals(SearchPredicate[]? x, SearchPredicate[]? y)
{
return (x, y) switch
{
(null, null) => true,
(null, _) => false,
(_, null) => false,
_ => x.SequenceEqual(y, DefaultComparer),
};
}
}
}
public record SearchPredicatePart(string Raw, string Text, bool MatchStart = false, bool MatchEnd = false)
{
public static SearchPredicatePart Parse(string part)
{
var raw = part;
var text = raw;

if (text.Length >= 2 && text.StartsWith('"') && text.EndsWith('"'))
{
// within quoted string, unquote and unescape \" to "
text = text
.Substring(1, text.Length - 2)
.Replace("\\\"", "\"");
}
if (text.StartsWith("^") is { } matchStart && matchStart)
{
text = text.Substring(1);
}
if (text.EndsWith("$") is { } matchEnd && matchEnd)
{
text = text.Substring(0, text.Length - 1);
}

return new(raw, text, matchStart, matchEnd);
}
}
}

#endif
Loading

0 comments on commit a3d454f

Please sign in to comment.