Ver código fonte

Fix random test failures and add empty dispatcher verification to tests (#17628)

* Add VerifyEmptyDispatcherAfterTestAttribute

* Use VerifyEmptyDispatcherAfterTest and fix failing tests

* Remove unsupported timeout from sync xUnit tests
Julien Lebosquain 10 meses atrás
pai
commit
1583de3e33
24 arquivos alterados com 141 adições e 44 exclusões
  1. 1 0
      Avalonia.sln.DotSettings
  2. 8 8
      build/XUnit.props
  3. 22 0
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  4. 13 2
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  5. 18 1
      src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs
  6. 2 2
      tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
  7. 1 1
      tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs
  8. 1 1
      tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
  9. 1 5
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs
  10. 3 3
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs
  11. 3 1
      tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs
  12. 5 1
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  13. 3 1
      tests/Avalonia.Controls.UnitTests/Properties/AssemblyInfo.cs
  14. 2 0
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  15. 3 3
      tests/Avalonia.Headless.UnitTests/InputTests.cs
  16. 4 4
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs
  17. 1 1
      tests/Avalonia.Headless.UnitTests/ServicesTests.cs
  18. 1 1
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs
  19. 4 4
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  20. 2 2
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  21. 3 1
      tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
  22. 1 1
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
  23. 1 1
      tests/Avalonia.UnitTests/InvariantCultureAttribute.cs
  24. 38 0
      tests/Avalonia.UnitTests/VerifyEmptyDispatcherAfterTestAttribute.cs

+ 1 - 0
Avalonia.sln.DotSettings

@@ -22,6 +22,7 @@
 	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /&gt;</s:String>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Interfaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /&gt;</s:String>

+ 8 - 8
build/XUnit.props

@@ -1,14 +1,14 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="xunit" Version="2.4.2" />
-    <PackageReference Include="xunit.assert" Version="2.4.2" />
-    <PackageReference Include="xunit.core" Version="2.4.2" />
-    <PackageReference Include="xunit.extensibility.core" Version="2.4.2" />
-    <PackageReference Include="xunit.extensibility.execution" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.console" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
+    <PackageReference Include="xunit" Version="2.9.2" />
+    <PackageReference Include="xunit.assert" Version="2.9.2" />
+    <PackageReference Include="xunit.core" Version="2.9.2" />
+    <PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
+    <PackageReference Include="xunit.extensibility.execution" Version="2.9.2" />
+    <PackageReference Include="xunit.runner.console" Version="2.9.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
     <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
   </ItemGroup>
   <PropertyGroup>
     <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\avalonia.snk</AssemblyOriginatorKeyFile>

+ 22 - 0
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Threading;
 
@@ -270,4 +271,25 @@ public partial class Dispatcher
         lock (InstanceLock)
             return _queue.MaxPriority >= priority;
     }
+
+    /// <summary>
+    /// Gets all pending jobs, unordered, without removing them.
+    /// </summary>
+    /// <remarks>Only use between unit tests!</remarks>
+    /// <returns>A list of jobs.</returns>
+    internal List<DispatcherOperation> GetJobs()
+    {
+        lock (InstanceLock)
+            return _queue.PeekAll();
+    }
+
+    /// <summary>
+    /// Clears all pending jobs.
+    /// </summary>
+    /// <remarks>Only use between unit tests!</remarks>
+    internal void ClearJobs()
+    {
+        lock (InstanceLock)
+            _queue.Clear();
+    }
 }

+ 13 - 2
src/Avalonia.Base/Threading/DispatcherOperation.cs

@@ -1,12 +1,13 @@
 using System;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.Runtime.CompilerServices;
-using System.Runtime.ExceptionServices;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace Avalonia.Threading;
 
+[DebuggerDisplay("{DebugDisplay}")]
 public class DispatcherOperation
 {
     protected readonly bool ThrowOnUiThread;
@@ -25,7 +26,7 @@ public class DispatcherOperation
         }
     }
 
-    protected object? Callback;
+    protected internal object? Callback;
     protected object? TaskSource;
     
     internal DispatcherOperation? SequentialPrev { get; set; }
@@ -53,6 +54,16 @@ public class DispatcherOperation
         Dispatcher = dispatcher;
     }
 
+    internal string DebugDisplay
+    {
+        get
+        {
+            var method = (Callback as Delegate)?.Method;
+            var methodDisplay = method is null ? "???" : method.DeclaringType + "." + method.Name;
+            return $"{methodDisplay} [{Priority}]";
+        }
+    }
+
     /// <summary>
     ///     An event that is raised when the operation is aborted or canceled.
     /// </summary>

+ 18 - 1
src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs

@@ -398,6 +398,23 @@ internal class DispatcherPriorityQueue
         // Step 3: cleanup
         item.SequentialPrev = item.SequentialNext = null;
     }
+
+    public List<DispatcherOperation> PeekAll()
+    {
+        var operations = new List<DispatcherOperation>();
+
+        for (var item = _head; item is not null; item = item.SequentialNext)
+            operations.Add(item);
+
+        return operations;
+    }
+
+    public void Clear()
+    {
+        _priorityChains.Clear();
+        _cacheReusableChains.Clear();
+        _head = _tail = null;
+    }
 }
 
 
@@ -415,4 +432,4 @@ internal class PriorityChain
     public DispatcherOperation? Head { get; set; }
 
     public DispatcherOperation? Tail { get; set; }
-}
+}

+ 2 - 2
tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs

@@ -15,7 +15,7 @@ using Xunit.Sdk;
 
 namespace Avalonia.Base.UnitTests.Composition;
 
-public class CompositionAnimationTests
+public class CompositionAnimationTests : ScopedTestBase
 {
 
     class AnimationDataProvider : DataAttribute
@@ -114,4 +114,4 @@ public class CompositionAnimationTests
             return Name;
         }
     }
-}
+}

+ 1 - 1
tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs

@@ -7,7 +7,7 @@ using Xunit;
 
 namespace Avalonia.Base.UnitTests.Input
 {
-    public class AccessKeyHandlerTests
+    public class AccessKeyHandlerTests : ScopedTestBase
     {
         [Fact]
         public void Should_Raise_Key_Events_For_Unregistered_Access_Key()

+ 1 - 1
tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs

@@ -14,7 +14,7 @@ using Moq;
 
 namespace Avalonia.Base.UnitTests.Input;
 
-public abstract class PointerTestsBase
+public abstract class PointerTestsBase : ScopedTestBase
 {
     private protected static void SetHit(Mock<IHitTester> renderer, Control? hit)
     {

+ 1 - 5
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs

@@ -11,7 +11,7 @@ using Xunit;
 
 namespace Avalonia.Base.UnitTests.Layout
 {
-    public class LayoutableTests_EffectiveViewportChanged
+    public class LayoutableTests_EffectiveViewportChanged : ScopedTestBase
     {
         [Fact]
         public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree_And_Layout_Pass_Has_Not_Run()
@@ -38,9 +38,7 @@ namespace Avalonia.Base.UnitTests.Layout
         [Fact]
         public async Task EffectiveViewportChanged_Raised_When_Control_Added_To_Tree_And_Layout_Pass_Has_Run()
         {
-#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
             await RunOnUIThread.Execute(async () =>
-#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
             {
                 var root = CreateRoot();
                 var target = new Canvas();
@@ -64,9 +62,7 @@ namespace Avalonia.Base.UnitTests.Layout
         [Fact]
         public async Task EffectiveViewportChanged_Raised_When_Root_LayedOut_And_Then_Control_Added_To_Tree_And_Layout_Pass_Runs()
         {
-#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
             await RunOnUIThread.Execute(async () =>
-#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
             {
                 var root = CreateRoot();
                 var target = new Canvas();

+ 3 - 3
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs

@@ -6,7 +6,7 @@ using Xunit.Sdk;
 
 namespace Avalonia.Base.UnitTests.Layout
 {
-    public class LayoutableTests_LayoutRounding
+    public class LayoutableTests_LayoutRounding : ScopedTestBase
     {
         [Theory]
         [InlineData(100, 100)]
@@ -112,7 +112,7 @@ namespace Avalonia.Base.UnitTests.Layout
         {
             if (!expected.NearlyEquals(actual))
             {
-                throw new EqualException(expected, actual);
+                throw EqualException.ForMismatchedValues(expected, actual);
             }
         }
 
@@ -120,7 +120,7 @@ namespace Avalonia.Base.UnitTests.Layout
         {
             if (!expected.NearlyEquals(actual))
             {
-                throw new EqualException(expected, actual);
+                throw EqualException.ForMismatchedValues(expected, actual);
             }
         }
 

+ 3 - 1
tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs

@@ -1,7 +1,9 @@
 using System.Reflection;
+using Avalonia.UnitTests;
 using Xunit;
 
-[assembly: AssemblyTitle("Avalonia.UnitTests")]
+[assembly: AssemblyTitle("Avalonia.Base.UnitTests")]
 
 // Don't run tests in parallel.
 [assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: VerifyEmptyDispatcherAfterTest]

+ 5 - 1
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -358,8 +358,10 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void FlowDirection_Of_RectangleContent_Shuold_Be_LeftToRight()
+        public void FlowDirection_Of_RectangleContent_Should_Be_LeftToRight()
         {
+            using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
             var target = new ComboBox
             {
                 FlowDirection = FlowDirection.RightToLeft,
@@ -385,6 +387,8 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void FlowDirection_Of_RectangleContent_Updated_After_InvalidateMirrorTransform()
         {
+            using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
             var parentContent = new Decorator()
             {
                 Child = new Control()

+ 3 - 1
tests/Avalonia.Controls.UnitTests/Properties/AssemblyInfo.cs

@@ -1,7 +1,9 @@
 using System.Reflection;
+using Avalonia.UnitTests;
 using Xunit;
 
 [assembly: AssemblyTitle("Avalonia.Controls.UnitTests")]
 
 // Don't run tests in parallel.
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: VerifyEmptyDispatcherAfterTest]

+ 2 - 0
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -408,6 +408,8 @@ namespace Avalonia.Controls.UnitTests
         [Fact]
         public void Previous_ContentTemplate_Is_Not_Reused_When_TabItem_Changes()
         {
+            using var app = UnitTestApplication.Start(TestServices.StyledWindow);
+
             int templatesBuilt = 0;
 
             var target = new TabControl

+ 3 - 3
tests/Avalonia.Headless.UnitTests/InputTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Reactive.Disposables;
 using System.Threading;
+using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Layout;
@@ -35,12 +36,11 @@ public class InputTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Click_Button_On_Window()
     {
         Assert.True(_setupApp == Application.Current);
-        
         var buttonClicked = false;
         var button = new Button
         {
@@ -62,7 +62,7 @@ public class InputTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Change_Window_Position()
     {

+ 4 - 4
tests/Avalonia.Headless.UnitTests/RenderingTests.cs

@@ -14,7 +14,7 @@ public class RenderingTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Render_Last_Frame_To_Bitmap()
     {
@@ -43,7 +43,7 @@ public class RenderingTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Not_Crash_On_GeometryGroup()
     {
@@ -79,7 +79,7 @@ public class RenderingTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Not_Crash_On_CombinedGeometry()
     {
@@ -110,7 +110,7 @@ public class RenderingTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Not_Hang_With_Non_Trivial_Layout()
     {

+ 1 - 1
tests/Avalonia.Headless.UnitTests/ServicesTests.cs

@@ -13,7 +13,7 @@ public class ServicesTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Can_Access_Screens()
     {

+ 1 - 1
tests/Avalonia.Headless.UnitTests/ThreadingTests.cs

@@ -12,7 +12,7 @@ public class ThreadingTests
 #if NUNIT
     [AvaloniaTest, Timeout(10000)]
 #elif XUNIT
-    [AvaloniaFact(Timeout = 10000)]
+    [AvaloniaFact]
 #endif
     public void Should_Be_On_Dispatcher_Thread()
     {

+ 4 - 4
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@@ -411,16 +411,16 @@ namespace Avalonia.IntegrationTests.Appium
                 // the position of a centered window can be off by a bit. From initial testing, looks
                 // like this shouldn't be more than 10 pixels.
                 if (Math.Abs(expected.X - actual.X) > 10)
-                    throw new EqualException(expected, actual);
+                    throw EqualException.ForMismatchedValues(expected, actual);
                 if (Math.Abs(expected.Y - actual.Y) > 10)
-                    throw new EqualException(expected, actual);
+                    throw EqualException.ForMismatchedValues(expected, actual);
             }
             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
                 if (Math.Abs(expected.X - actual.X) > 15)
-                    throw new EqualException(expected, actual);
+                    throw EqualException.ForMismatchedValues(expected, actual);
                 if (Math.Abs(expected.Y - actual.Y) > 15)
-                    throw new EqualException(expected, actual);
+                    throw EqualException.ForMismatchedValues(expected, actual);
             }
             else
             {

+ 2 - 2
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -37,11 +37,11 @@
     </Compile>
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Update="xunit.runner.console" Version="2.7.0">
+    <PackageReference Update="xunit.runner.console" Version="2.9.2">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Update="xunit.runner.visualstudio" Version="2.5.7">
+    <PackageReference Update="xunit.runner.visualstudio" Version="2.8.2">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 3 - 1
tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs

@@ -1,6 +1,8 @@
+using Avalonia.UnitTests;
 using Xunit;
 
 // Required to avoid InvalidOperationException sometimes thrown
 // from Splat.MemoizingMRUCache.cs which is not thread-safe.
 // Thrown when trying to access WhenActivated concurrently.
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: VerifyEmptyDispatcherAfterTest]

+ 1 - 1
tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs

@@ -8,7 +8,7 @@ using Xunit;
 
 namespace Avalonia.ReactiveUI.UnitTests
 {
-    public class ReactiveUserControlTest
+    public class ReactiveUserControlTest : ScopedTestBase
     {
         public class ExampleViewModel : ReactiveObject, IActivatableViewModel
         {

+ 1 - 1
tests/Avalonia.UnitTests/InvariantCultureAttribute.cs

@@ -15,7 +15,7 @@ namespace Avalonia.UnitTests;
 /// Some tests are formatting numbers, expecting a dot as a decimal point.
 /// Use this fixture to set the current culture to the invariant culture.
 /// </remarks>
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
 public sealed class InvariantCultureAttribute : BeforeAfterTestAttribute
 {
     private CultureInfo? _previousCulture;

+ 38 - 0
tests/Avalonia.UnitTests/VerifyEmptyDispatcherAfterTestAttribute.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Avalonia.UnitTests;
+
+public sealed class VerifyEmptyDispatcherAfterTestAttribute : BeforeAfterTestAttribute
+{
+    public override void After(MethodInfo methodUnderTest)
+    {
+        if (typeof(ScopedTestBase).IsAssignableFrom(methodUnderTest.DeclaringType))
+            return;
+
+        var dispatcher = Dispatcher.UIThread;
+        var jobs = dispatcher.GetJobs();
+        if (jobs.Count == 0)
+            return;
+
+        dispatcher.ClearJobs();
+
+        // Ignore the Control.Loaded callback. It might happen synchronously or might be posted.
+        if (jobs.Count == 1 && IsLoadedCallback(jobs[0]))
+            return;
+
+        Assert.Fail(
+            $"The test left {jobs.Count} unprocessed dispatcher {(jobs.Count == 1 ? "job" : "jobs")}:\n" +
+            $"{string.Join(Environment.NewLine, jobs.Select(job => $"  - {job.DebugDisplay}"))}\n" +
+            $"Consider using ScopedTestBase or UnitTestApplication.Start().");
+
+        static bool IsLoadedCallback(DispatcherOperation job)
+            => job.Priority == DispatcherPriority.Loaded &&
+               (job.Callback as Delegate)?.Method.DeclaringType?.DeclaringType == typeof(Control);
+    }
+}