Browse Source

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
pull/17657/head
Julien Lebosquain 1 year ago
committed by GitHub
parent
commit
1583de3e33
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.sln.DotSettings
  2. 16
      build/XUnit.props
  3. 22
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  4. 15
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  5. 17
      src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs
  6. 2
      tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
  7. 2
      tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs
  8. 2
      tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
  9. 6
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs
  10. 6
      tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs
  11. 4
      tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs
  12. 6
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  13. 2
      tests/Avalonia.Controls.UnitTests/Properties/AssemblyInfo.cs
  14. 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  15. 6
      tests/Avalonia.Headless.UnitTests/InputTests.cs
  16. 8
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs
  17. 2
      tests/Avalonia.Headless.UnitTests/ServicesTests.cs
  18. 2
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs
  19. 8
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  20. 4
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  21. 2
      tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
  22. 2
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
  23. 2
      tests/Avalonia.UnitTests/InvariantCultureAttribute.cs
  24. 38
      tests/Avalonia.UnitTests/VerifyEmptyDispatcherAfterTestAttribute.cs

1
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/=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/@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/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/=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/=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> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Interfaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /&gt;</s:String>

16
build/XUnit.props

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

22
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
@ -270,4 +271,25 @@ public partial class Dispatcher
lock (InstanceLock) lock (InstanceLock)
return _queue.MaxPriority >= priority; 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();
}
} }

15
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -1,12 +1,13 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Avalonia.Threading; namespace Avalonia.Threading;
[DebuggerDisplay("{DebugDisplay}")]
public class DispatcherOperation public class DispatcherOperation
{ {
protected readonly bool ThrowOnUiThread; protected readonly bool ThrowOnUiThread;
@ -25,7 +26,7 @@ public class DispatcherOperation
} }
} }
protected object? Callback; protected internal object? Callback;
protected object? TaskSource; protected object? TaskSource;
internal DispatcherOperation? SequentialPrev { get; set; } internal DispatcherOperation? SequentialPrev { get; set; }
@ -53,6 +54,16 @@ public class DispatcherOperation
Dispatcher = dispatcher; 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> /// <summary>
/// An event that is raised when the operation is aborted or canceled. /// An event that is raised when the operation is aborted or canceled.
/// </summary> /// </summary>

17
src/Avalonia.Base/Threading/DispatcherPriorityQueue.cs

@ -398,6 +398,23 @@ internal class DispatcherPriorityQueue
// Step 3: cleanup // Step 3: cleanup
item.SequentialPrev = item.SequentialNext = null; 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;
}
} }

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

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

2
tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs

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

2
tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs

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

6
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_EffectiveViewportChanged.cs

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

6
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs

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

4
tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs

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

6
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

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

2
tests/Avalonia.Controls.UnitTests/Properties/AssemblyInfo.cs

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

2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

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

6
tests/Avalonia.Headless.UnitTests/InputTests.cs

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

8
tests/Avalonia.Headless.UnitTests/RenderingTests.cs

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

2
tests/Avalonia.Headless.UnitTests/ServicesTests.cs

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

2
tests/Avalonia.Headless.UnitTests/ThreadingTests.cs

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

8
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 // 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. // like this shouldn't be more than 10 pixels.
if (Math.Abs(expected.X - actual.X) > 10) 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) if (Math.Abs(expected.Y - actual.Y) > 10)
throw new EqualException(expected, actual); throw EqualException.ForMismatchedValues(expected, actual);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
if (Math.Abs(expected.X - actual.X) > 15) 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) if (Math.Abs(expected.Y - actual.Y) > 15)
throw new EqualException(expected, actual); throw EqualException.ForMismatchedValues(expected, actual);
} }
else else
{ {

4
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

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

2
tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs

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

2
tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs

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

2
tests/Avalonia.UnitTests/InvariantCultureAttribute.cs

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

38
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);
}
}
Loading…
Cancel
Save