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
This commit is contained in:
Julien Lebosquain 2024-11-30 13:25:31 +01:00 коммит произвёл GitHub
Родитель e8ce57871e
Коммит 1583de3e33
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
24 изменённых файлов: 141 добавлений и 44 удалений

Просмотреть файл

@ -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>

Просмотреть файл

@ -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>

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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>

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -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;
}
}
}
}

Просмотреть файл

@ -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()

Просмотреть файл

@ -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)
{

Просмотреть файл

@ -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();

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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]

Просмотреть файл

@ -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()

Просмотреть файл

@ -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]

Просмотреть файл

@ -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

Просмотреть файл

@ -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()
{

Просмотреть файл

@ -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()
{

Просмотреть файл

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

Просмотреть файл

@ -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()
{

Просмотреть файл

@ -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
{

Просмотреть файл

@ -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>

Просмотреть файл

@ -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]

Просмотреть файл

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

Просмотреть файл

@ -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;

Просмотреть файл

@ -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);
}
}