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 15, 2023
1 parent cbf16c3 commit 4b37deb
Show file tree
Hide file tree
Showing 15 changed files with 643 additions and 160 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_
86 changes: 86 additions & 0 deletions src/TestApp/shared/EngineFeatureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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)
{
}

[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" };
}
}
}
21 changes: 21 additions & 0 deletions src/TestApp/shared/Extensions/AssertExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Uno.UI.RuntimeTests.Engine.Extensions;

internal static class AssertExtensions
{
public static void AreEqual<T>(this Assert assert, T expected, T actual, IEqualityComparer<T> comparer)
{
if (!comparer.Equals(expected, actual))
{
Assert.Fail(string.Join("\n",
"AreEqual failed.",
$"Expected: {expected}",
$"Actual: {actual}"
));
}
}
}
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
123 changes: 123 additions & 0 deletions src/TestApp/shared/SearchQueryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.UI.RuntimeTests.Engine.Extensions;
using static Uno.UI.RuntimeTests.UnitTestsControl;

namespace Uno.UI.RuntimeTests.Engine;

[TestClass]
public partial class SearchQueryTests
{
[TestMethod]
[DataRow("asd")] // simple
[DataRow("^asd")] // match start
[DataRow("asd$")] // match end
[DataRow("^asd$")] // full match
public void When_SearchPredicatePart_Parse(string input)
{
var actual = SearchPredicatePart.Parse(input);
var expected = input switch
{
"asd" => new SearchPredicatePart(input, "asd"),
"^asd" => new SearchPredicatePart(input, "asd", MatchStart: true),
"asd$" => new SearchPredicatePart(input, "asd", MatchEnd: true),
"^asd$" => new SearchPredicatePart(input, "asd", MatchStart: true, MatchEnd: true),

_ => throw new ArgumentOutOfRangeException(input),
};

Assert.AreEqual(expected, actual);
}

[TestMethod]
[DataRow("asd")] // simple
[DataRow("^asd,qwe$,^zxc$")] // multi parts
[DataRow("-asd")] // exclusion
[DataRow("tag:asd")] // tagged
public void When_SearchPredicate_ParseFragment(string input)
{
var actual = SearchPredicate.ParseFragment(input);
var expected = input switch
{
"asd" => new SearchPredicate("asd", "asd", Parts: new SearchPredicatePart("asd", "asd")),
"^asd,qwe$,^zxc$" => new SearchPredicate("^asd,qwe$,^zxc$", "^asd,qwe$,^zxc$", Parts: new[]{
new SearchPredicatePart("^asd", "asd", MatchStart: true),
new SearchPredicatePart("qwe$", "qwe", MatchEnd: true),
new SearchPredicatePart("^zxc$", "zxc", MatchStart: true, MatchEnd: true),
}),
"-asd" => new SearchPredicate("-asd", "asd", Exclusion: true, Parts: new[]{
new SearchPredicatePart("asd", "asd"),
}),
"tag:asd" => new SearchPredicate("tag:asd", "asd", Tag: "tag", Parts: new[]{
new SearchPredicatePart("asd", "asd"),
}),

_ => throw new ArgumentOutOfRangeException(input),
};

Assert.That.AreEqual(expected, actual, SearchPredicate.DefaultComparer);
}

[TestMethod]
[DataRow("asd")] // simple
[DataRow("asd qwe")] // multi fragments
[DataRow("class:asd method:qwe display_name:zxc -123")] // tags
[DataRow("c:asd m:qwe d:zxc -123")] // aliased tags
[DataRow("d:\"^asd \\\", asd$\"")] // quoted with escape
[DataRow("at:asd\"asd\"asd,^asd,asd$")] // inner literal quote
[DataRow("asd @0")] // custom prefix
[DataRow("p:\"index 0\",\"index 1\" -p:\"asd\"")] // multiple quotes

public void When_SearchPredicate_Parse(string input)
{
var actual = SearchPredicate.ParseQuery(input);
var predicates = input switch
{
"asd" => new SearchPredicate[] { new("asd", "asd", Parts: new SearchPredicatePart[] { new("asd", "asd"), }), },
"asd qwe" => new SearchPredicate[] {
new("asd", "asd", Parts: new SearchPredicatePart[] { new("asd", "asd"), }),
new("qwe", "qwe", Parts: new SearchPredicatePart[] { new("qwe", "qwe"), }),
},
"class:asd method:qwe display_name:zxc -123" => new SearchPredicate[] {
new("class:asd", "asd", Tag: "class", Parts: new SearchPredicatePart[] { new("asd", "asd"), }),
new("method:qwe", "qwe", Tag: "method", Parts: new SearchPredicatePart[] { new("qwe", "qwe"), }),
new("display_name:zxc", "zxc", Tag: "display_name", Parts: new SearchPredicatePart[] { new("zxc", "zxc"), }),
new("-123", "123", Exclusion: true, Parts: new SearchPredicatePart[] { new("123", "123"), }),
},
"c:asd m:qwe d:zxc -123" => new SearchPredicate[] {
new("c:asd", "asd", Tag: "class", Parts: new SearchPredicatePart[] { new("asd", "asd"), }),
new("m:qwe", "qwe", Tag: "method", Parts: new SearchPredicatePart[] { new("qwe", "qwe"), }),
new("d:zxc", "zxc", Tag: "display_name", Parts: new SearchPredicatePart[] { new("zxc", "zxc"), }),
new("-123", "123", Exclusion: true, Parts: new SearchPredicatePart[] { new("123", "123"), }),
},
"d:\"^asd \\\", asd$\"" => new SearchPredicate[] {
new("d:\"^asd \\\", asd$\"", "\"^asd \\\", asd$\"", Tag: "display_name", Parts: new SearchPredicatePart[] {
new("\"^asd \\\", asd$\"", "asd \", asd", MatchStart: true, MatchEnd: true),
}),
},
"at:asd\"asd\"asd,^asd,asd$" => new SearchPredicate[] {
new("at:asd\"asd\"asd,^asd,asd$", "asd\"asd\"asd,^asd,asd$", Tag: "at", Parts: new SearchPredicatePart[] {
new("asd\"asd\"asd", "asd\"asd\"asd"),
new ("^asd", "asd", MatchStart: true),
new ("asd$", "asd", MatchEnd: true),
}),
},
"asd @0" => new SearchPredicate[] {
new("asd", "asd", Parts: new SearchPredicatePart[] { new ("asd", "asd"), }),
new("@0", "0", Tag: "at", Parts: new SearchPredicatePart[] { new ("0", "0"), }),
},
"p:\"index 0\",\"index 1\" -p:\"asd\"" => new SearchPredicate[] {
new("p:\"index 0\",\"index 1\"", "\"index 0\",\"index 1\"", Tag: "params", Parts: new SearchPredicatePart[] { new("\"index 0\"", "index 0"), new("\"index 1\"", "index 1") }),
new("-p:\"asd\"", "\"asd\"", Exclusion: true, Tag: "params", Parts: new SearchPredicatePart[] { new("\"asd\"", "asd") })
},

_ => throw new ArgumentOutOfRangeException(input),
};
var expected = new SearchPredicateCollection(predicates!);

Assert.That.AreEqual(expected, actual, SearchPredicate.DefaultQueryComparer);
}
}
2 changes: 2 additions & 0 deletions src/TestApp/shared/Uno.UI.RuntimeTests.Engine.Shared.shproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<Import Project="Uno.UI.RuntimeTests.Engine.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
<ItemGroup>
<_Globbed_Compile Remove="Extensions\AssertExtensions.cs" />
<_Globbed_Compile Remove="MetaAttributes.cs" />
<_Globbed_Compile Remove="SearchQueryTests.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
<Compile Include="$(MSBuildThisFileDirectory)App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AssertExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)MetaAttributes.cs" />
<Compile Include="$(MSBuildThisFileDirectory)EngineFeatureTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SearchQueryTests.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 is { } afterIgnore && afterIgnore < input.Length
? input.IndexOf(separator, afterIgnore)
: -1;
}

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

// skip multiple continuous 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);
}
}
Loading

0 comments on commit 4b37deb

Please sign in to comment.