зеркало из https://github.com/AvaloniaUI/Avalonia.git
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:
Родитель
e8ce57871e
Коммит
1583de3e33
|
@ -22,6 +22,7 @@
|
|||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=TYPEDEF/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/UserRules/=UNION_005FMEMBER/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></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"><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue"><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Interfaces/@EntryIndexedValue"><Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /></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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче