Skip to content

Commit 8ce91be

Browse files
committed
feat: improved query syntax
1 parent cbf16c3 commit 8ce91be

8 files changed

Lines changed: 160 additions & 40 deletions

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,22 @@ and define the following in your `csproj`:
161161
```
162162
These attributes will ask for the runtime test engine to replace the ones defined by the `Uno.UI.RuntimeTests.Engine` package.
163163

164+
## Test runner (UnitTestsControl) filtering syntax
165+
- Search terms are separated by space. Multiple consecutive spaces are treated same as one.
166+
- Multiple search terms are chained with AND logic.
167+
- Search terms are case insensitive.
168+
- `-` can be used before any term for exclusion, effectively inverting the results.
169+
- Special tags can be used to match certain part of the test: // syntax: tag:term
170+
- `class` or `c` matches the class name
171+
- `method` or `m` matches the method name
172+
- `displayname` or `d` matches the display name in [DataRow]
173+
- Search term without a prefixing tag will match either of method name or class name.
174+
175+
Examples:
176+
- `listview`
177+
- `listview measure`
178+
- `listview measure -recycle`
179+
- `c:listview m:measure -m:recycle`
180+
164181
## Running the tests automatically during CI
165182
_TBD_
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
#if HAS_UNO_WINUI || WINDOWS_WINUI
8+
using Microsoft.UI.Xaml;
9+
using Microsoft.UI.Xaml.Controls;
10+
#else
11+
using Windows.UI.Xaml;
12+
using Windows.UI.Xaml.Controls;
13+
#endif
14+
15+
namespace Uno.UI.RuntimeTests.Engine
16+
{
17+
/// <summary>
18+
/// Contains tests relevant to the RTT engine features.
19+
/// </summary>
20+
[TestClass]
21+
public class MetaTests
22+
{
23+
[TestMethod]
24+
[RunsOnUIThread]
25+
public async Task When_Test_ContentHelper()
26+
{
27+
var SUT = new TextBlock() { Text = "Hello" };
28+
UnitTestsUIContentHelper.Content = SUT;
29+
30+
await UnitTestsUIContentHelper.WaitForIdle();
31+
await UnitTestsUIContentHelper.WaitForLoaded(SUT);
32+
}
33+
34+
[TestMethod]
35+
[DataRow("hello", DisplayName = "hello test")]
36+
[DataRow("goodbye", DisplayName = "goodbye test")]
37+
public void When_DisplayName(string text)
38+
{
39+
}
40+
}
41+
}

src/TestApp/shared/SanityTests.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
namespace Uno.UI.RuntimeTests.Engine
1616
{
17+
/// <summary>
18+
/// Contains sanity/smoke tests used to assert basic scenarios.
19+
/// </summary>
1720
[TestClass]
1821
public class SanityTests
1922
{
@@ -28,24 +31,6 @@ public async Task Is_Still_Sane()
2831
await Task.Delay(2000);
2932
}
3033

31-
[TestMethod]
32-
[RunsOnUIThread]
33-
public async Task When_Test_ContentHelper()
34-
{
35-
var SUT = new TextBlock() { Text = "Hello" };
36-
UnitTestsUIContentHelper.Content = SUT;
37-
38-
await UnitTestsUIContentHelper.WaitForIdle();
39-
await UnitTestsUIContentHelper.WaitForLoaded(SUT);
40-
}
41-
42-
[TestMethod]
43-
[DataRow("hello", DisplayName = "hello test")]
44-
[DataRow("goodbye", DisplayName = "goodbye test")]
45-
public void Is_Sane_With_Cases(string text)
46-
{
47-
}
48-
4934
#if DEBUG
5035
[TestMethod]
5136
public async Task No_Longer_Sane() // expected to fail

src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<DependentUpon>MainPage.xaml</DependentUpon>
2323
</Compile>
2424
<Compile Include="$(MSBuildThisFileDirectory)MetaAttributes.cs" />
25+
<Compile Include="$(MSBuildThisFileDirectory)EngineFeatureTests.cs" />
2526
</ItemGroup>
2627
<ItemGroup>
2728
<Page Include="$(MSBuildThisFileDirectory)MainPage.xaml">

src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestEngineConfig.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class UnitTestEngineConfig
1010

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

13-
public string[]? Filters { get; set; }
13+
public string? Query { get; set; }
1414

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#if !UNO_RUNTIMETESTS_DISABLE_UI
2+
3+
#nullable enable
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Data;
8+
using System.Linq;
9+
using System.Reflection;
10+
using System.Text;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting;
12+
using static System.StringComparison;
13+
14+
namespace Uno.UI.RuntimeTests;
15+
16+
partial class UnitTestsControl
17+
{
18+
private static IEnumerable<MethodInfo> FilterTests(UnitTestClassInfo testClassInfo, string? query)
19+
{
20+
var tests = testClassInfo.Tests?.AsEnumerable() ?? Array.Empty<MethodInfo>();
21+
foreach (var filter in SearchPredicate.ParseQuery(query))
22+
{
23+
// chain filters with AND logic
24+
tests = tests.Where(x =>
25+
filter.Exclusion ^ // use xor to flip the result based on Exclusion
26+
filter.Tag?.ToLowerInvariant() switch
27+
{
28+
"class" => MatchClassName(x, filter.Text),
29+
"displayname" => MatchDisplayName(x, filter.Text),
30+
"method" => MatchMethodName(x, filter.Text),
31+
32+
_ => MatchClassName(x, filter.Text) || MatchMethodName(x, filter.Text),
33+
}
34+
);
35+
}
36+
37+
bool MatchClassName(MethodInfo x, string value) => x.DeclaringType?.Name.Contains(value, InvariantCultureIgnoreCase) ?? false;
38+
bool MatchMethodName(MethodInfo x, string value) => x.Name.Contains(value, InvariantCultureIgnoreCase);
39+
bool MatchDisplayName(MethodInfo x, string value) =>
40+
// fixme: since we are returning MethodInfo for match, there is no way to specify
41+
// which of the [DataRow] or which row within [DynamicData] without refactoring.
42+
// fixme: support [DynamicData]
43+
x.GetCustomAttributes<DataRowAttribute>().Any(y => y.DisplayName.Contains(value, InvariantCultureIgnoreCase));
44+
45+
return tests;
46+
}
47+
48+
public record SearchPredicate(string Raw, string Text, bool Exclusion, string? Tag = null)
49+
{
50+
private static readonly IReadOnlyDictionary<string, string> TagAliases =
51+
new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
52+
{
53+
["c"] = "class",
54+
["m"] = "method",
55+
["d"] = "displayname",
56+
};
57+
58+
public static SearchPredicate[] ParseQuery(string? query)
59+
{
60+
if (string.IsNullOrWhiteSpace(query)) return Array.Empty<SearchPredicate>();
61+
62+
return query!.Split(' ', StringSplitOptions.RemoveEmptyEntries)
63+
.Select(Parse)
64+
.OfType<SearchPredicate>() // trim null
65+
.Where(x => x.Text.Length > 0) // ignore empty tag query eg: "c:"
66+
.ToArray();
67+
}
68+
69+
public static SearchPredicate? Parse(string criteria)
70+
{
71+
if (string.IsNullOrWhiteSpace(criteria)) return null;
72+
73+
var raw = criteria.Trim();
74+
var text = raw;
75+
if (text.StartsWith('-') is var exclusion && exclusion)
76+
{
77+
text = text.Substring(1);
78+
}
79+
var tag = default(string?);
80+
if (text.Split(':', 2) is { Length: 2 } tagParts)
81+
{
82+
tag = TagAliases.TryGetValue(tagParts[0], out var alias) ? alias : tagParts[0];
83+
text = tagParts[1];
84+
}
85+
86+
return new(raw, text, exclusion, tag);
87+
}
88+
}
89+
}
90+
91+
#endif

src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public sealed partial class UnitTestsControl : UserControl
6565
#endif
6666
#pragma warning restore CS0109
6767

68-
private const StringComparison StrComp = StringComparison.InvariantCultureIgnoreCase;
6968
private Task? _runner;
7069
private CancellationTokenSource? _cts = new CancellationTokenSource();
7170
#if DEBUG
@@ -499,7 +498,7 @@ private void EnableConfigPersistence()
499498
consoleOutput.IsChecked = config.IsConsoleOutputEnabled;
500499
runIgnored.IsChecked = config.IsRunningIgnored;
501500
retry.IsChecked = config.Attempts > 1;
502-
testFilter.Text = string.Join(";", config.Filters ?? Array.Empty<string>());
501+
testFilter.Text = config.Query ?? string.Empty;
503502
}
504503
}
505504
catch (Exception)
@@ -534,15 +533,11 @@ private UnitTestEngineConfig BuildConfig()
534533
var isConsoleOutput = consoleOutput.IsChecked ?? false;
535534
var isRunningIgnored = runIgnored.IsChecked ?? false;
536535
var attempts = (retry.IsChecked ?? true) ? UnitTestEngineConfig.DefaultRepeatCount : 1;
537-
var filter = testFilter.Text.Trim();
538-
if (string.IsNullOrEmpty(filter))
539-
{
540-
filter = null;
541-
}
536+
var query = testFilter.Text.Trim() is { Length: >0 } text ? text: null;
542537

543538
return new UnitTestEngineConfig
544539
{
545-
Filters = filter?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>(),
540+
Query = query,
546541
IsConsoleOutputEnabled = isConsoleOutput,
547542
IsRunningIgnored = isRunningIgnored,
548543
Attempts = attempts,
@@ -675,17 +670,6 @@ await _dispatcher.RunAsync(() =>
675670
await GenerateTestResults();
676671
}
677672

678-
private static IEnumerable<MethodInfo> FilterTests(UnitTestClassInfo testClassInfo, string[]? filters)
679-
{
680-
var testClassNameContainsFilters = filters?.Any(f => testClassInfo.Type?.FullName?.Contains(f, StrComp) ?? false) ?? false;
681-
return testClassInfo.Tests?.
682-
Where(t => ((!filters?.Any()) ?? true)
683-
|| testClassNameContainsFilters
684-
|| (filters?.Any(f => t.DeclaringType?.FullName?.Contains(f, StrComp) ?? false) ?? false)
685-
|| (filters?.Any(f => t.Name.Contains(f, StrComp)) ?? false))
686-
?? Array.Empty<MethodInfo>();
687-
}
688-
689673
private async Task ExecuteTestsForInstance(
690674
CancellationToken ct,
691675
object instance,
@@ -696,7 +680,7 @@ private async Task ExecuteTestsForInstance(
696680
? ConsoleOutputRecorder.Start()
697681
: default;
698682

699-
var tests = UnitTestsControl.FilterTests(testClassInfo, config.Filters)
683+
var tests = FilterTests(testClassInfo, config.Query)
700684
.Select(method => new UnitTestMethodInfo(instance, method))
701685
.ToArray();
702686

@@ -705,7 +689,7 @@ private async Task ExecuteTestsForInstance(
705689
return;
706690
}
707691

708-
ReportTestClass(testClassInfo.Type.GetTypeInfo());
692+
ReportTestClass(testClassInfo.Type!.GetTypeInfo());
709693
_ = ReportMessage($"Running {tests.Length} test methods");
710694

711695
foreach (var test in tests)

src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestEngineConfig.cs" />
2424
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestMethodInfo.cs" />
2525
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestsControl.cs" />
26+
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestsControl.Filtering.cs" />
2627
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestsControl.CustomConsoleOutput.cs" />
2728
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestsUIContentHelper.cs" />
2829
<Compile Include="$(MSBuildThisFileDirectory)UI\UnitTestDispatcherCompat.cs" />

0 commit comments

Comments
 (0)