From 8ce91be42bbcd8e5009dcff2edf278def7699d5d Mon Sep 17 00:00:00 2001 From: xiaoy312 Date: Wed, 12 Apr 2023 13:37:54 -0400 Subject: [PATCH] feat: improved query syntax --- README.md | 17 ++++ src/TestApp/shared/EngineFeatureTests.cs | 41 +++++++++ src/TestApp/shared/SanityTests.cs | 21 +---- ...no.ui.runtimetests.engine.Shared.projitems | 1 + .../UI/UnitTestEngineConfig.cs | 2 +- .../UI/UnitTestsControl.Filtering.cs | 91 +++++++++++++++++++ .../UI/UnitTestsControl.cs | 26 +----- ...o.UI.RuntimeTests.Engine.Library.projitems | 1 + 8 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 src/TestApp/shared/EngineFeatureTests.cs create mode 100644 src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.Filtering.cs diff --git a/README.md b/README.md index 730d41f..f525fd3 100644 --- a/README.md +++ b/README.md @@ -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_ diff --git a/src/TestApp/shared/EngineFeatureTests.cs b/src/TestApp/shared/EngineFeatureTests.cs new file mode 100644 index 0000000..3dc96e1 --- /dev/null +++ b/src/TestApp/shared/EngineFeatureTests.cs @@ -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 +{ + /// + /// Contains tests relevant to the RTT engine features. + /// + [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) + { + } + } +} diff --git a/src/TestApp/shared/SanityTests.cs b/src/TestApp/shared/SanityTests.cs index c2b0039..4a00eca 100644 --- a/src/TestApp/shared/SanityTests.cs +++ b/src/TestApp/shared/SanityTests.cs @@ -14,6 +14,9 @@ namespace Uno.UI.RuntimeTests.Engine { + /// + /// Contains sanity/smoke tests used to assert basic scenarios. + /// [TestClass] public class SanityTests { @@ -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 diff --git a/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems b/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems index 0083c1d..7632538 100644 --- a/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems +++ b/src/TestApp/shared/uno.ui.runtimetests.engine.Shared.projitems @@ -22,6 +22,7 @@ MainPage.xaml + diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestEngineConfig.cs b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestEngineConfig.cs index c66d02f..a19f6f3 100644 --- a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestEngineConfig.cs +++ b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestEngineConfig.cs @@ -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; diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.Filtering.cs b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.Filtering.cs new file mode 100644 index 0000000..fdc3b40 --- /dev/null +++ b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.Filtering.cs @@ -0,0 +1,91 @@ +#if !UNO_RUNTIMETESTS_DISABLE_UI + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static System.StringComparison; + +namespace Uno.UI.RuntimeTests; + +partial class UnitTestsControl +{ + private static IEnumerable FilterTests(UnitTestClassInfo testClassInfo, string? query) + { + var tests = testClassInfo.Tests?.AsEnumerable() ?? Array.Empty(); + 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().Any(y => y.DisplayName.Contains(value, InvariantCultureIgnoreCase)); + + return tests; + } + + public record SearchPredicate(string Raw, string Text, bool Exclusion, string? Tag = null) + { + private static readonly IReadOnlyDictionary TagAliases = + new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["c"] = "class", + ["m"] = "method", + ["d"] = "displayname", + }; + + public static SearchPredicate[] ParseQuery(string? query) + { + if (string.IsNullOrWhiteSpace(query)) return Array.Empty(); + + return query!.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(Parse) + .OfType() // trim null + .Where(x => x.Text.Length > 0) // ignore empty tag query eg: "c:" + .ToArray(); + } + + public static SearchPredicate? Parse(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 = TagAliases.TryGetValue(tagParts[0], out var alias) ? alias : tagParts[0]; + text = tagParts[1]; + } + + return new(raw, text, exclusion, tag); + } + } +} + +#endif \ No newline at end of file diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs index 4a8c27b..c810020 100644 --- a/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs +++ b/src/Uno.UI.RuntimeTests.Engine.Library/UI/UnitTestsControl.cs @@ -65,7 +65,6 @@ public sealed partial class UnitTestsControl : UserControl #endif #pragma warning restore CS0109 - private const StringComparison StrComp = StringComparison.InvariantCultureIgnoreCase; private Task? _runner; private CancellationTokenSource? _cts = new CancellationTokenSource(); #if DEBUG @@ -499,7 +498,7 @@ private void EnableConfigPersistence() consoleOutput.IsChecked = config.IsConsoleOutputEnabled; runIgnored.IsChecked = config.IsRunningIgnored; retry.IsChecked = config.Attempts > 1; - testFilter.Text = string.Join(";", config.Filters ?? Array.Empty()); + testFilter.Text = config.Query ?? string.Empty; } } catch (Exception) @@ -534,15 +533,11 @@ private UnitTestEngineConfig BuildConfig() var isConsoleOutput = consoleOutput.IsChecked ?? false; var isRunningIgnored = runIgnored.IsChecked ?? false; var attempts = (retry.IsChecked ?? true) ? UnitTestEngineConfig.DefaultRepeatCount : 1; - var filter = testFilter.Text.Trim(); - if (string.IsNullOrEmpty(filter)) - { - filter = null; - } + var query = testFilter.Text.Trim() is { Length: >0 } text ? text: null; return new UnitTestEngineConfig { - Filters = filter?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(), + Query = query, IsConsoleOutputEnabled = isConsoleOutput, IsRunningIgnored = isRunningIgnored, Attempts = attempts, @@ -675,17 +670,6 @@ await _dispatcher.RunAsync(() => await GenerateTestResults(); } - private static IEnumerable FilterTests(UnitTestClassInfo testClassInfo, string[]? filters) - { - var testClassNameContainsFilters = filters?.Any(f => testClassInfo.Type?.FullName?.Contains(f, StrComp) ?? false) ?? false; - return testClassInfo.Tests?. - Where(t => ((!filters?.Any()) ?? true) - || testClassNameContainsFilters - || (filters?.Any(f => t.DeclaringType?.FullName?.Contains(f, StrComp) ?? false) ?? false) - || (filters?.Any(f => t.Name.Contains(f, StrComp)) ?? false)) - ?? Array.Empty(); - } - private async Task ExecuteTestsForInstance( CancellationToken ct, object instance, @@ -696,7 +680,7 @@ private async Task ExecuteTestsForInstance( ? ConsoleOutputRecorder.Start() : default; - var tests = UnitTestsControl.FilterTests(testClassInfo, config.Filters) + var tests = FilterTests(testClassInfo, config.Query) .Select(method => new UnitTestMethodInfo(instance, method)) .ToArray(); @@ -705,7 +689,7 @@ private async Task ExecuteTestsForInstance( return; } - ReportTestClass(testClassInfo.Type.GetTypeInfo()); + ReportTestClass(testClassInfo.Type!.GetTypeInfo()); _ = ReportMessage($"Running {tests.Length} test methods"); foreach (var test in tests) diff --git a/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems b/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems index 3c7c839..9434614 100644 --- a/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems +++ b/src/Uno.UI.RuntimeTests.Engine.Library/Uno.UI.RuntimeTests.Engine.Library.projitems @@ -23,6 +23,7 @@ +