123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Diagnostics.CodeAnalysis;
- using System.Globalization;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using Avalonia.Controls.Platform;
- using Avalonia.Threading;
- using Avalonia.Utilities;
- using Xunit;
- namespace Avalonia.Base.UnitTests;
- public partial class DispatcherTests
- {
- class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput
- {
- private readonly Thread _loopThread = Thread.CurrentThread;
- private readonly object _lock = new();
- public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _loopThread;
- public void Signal()
- {
- lock (_lock)
- AskedForSignal = true;
- }
- public event Action Signaled;
- public event Action Timer;
- public long? NextTimer { get; private set; }
- public bool AskedForSignal { get; private set; }
- public void UpdateTimer(long? dueTimeInTicks)
- {
- NextTimer = dueTimeInTicks;
- }
- public long Now { get; set; }
- public void ExecuteSignal()
- {
- lock (_lock)
- {
- if (!AskedForSignal)
- return;
- AskedForSignal = false;
- }
- Signaled?.Invoke();
- }
- public void ExecuteTimer()
- {
- if (NextTimer == null)
- return;
- Now = NextTimer.Value;
- Timer?.Invoke();
- }
- public bool CanQueryPendingInput => TestInputPending != null;
- public bool HasPendingInput => TestInputPending == true;
- public bool? TestInputPending { get; set; }
- }
- class SimpleDispatcherWithBackgroundProcessingImpl : SimpleDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing
- {
- public bool AskedForBackgroundProcessing { get; private set; }
- public event Action ReadyForBackgroundProcessing;
- public void RequestBackgroundProcessing()
- {
- if (!CurrentThreadIsLoopThread)
- throw new InvalidOperationException();
- AskedForBackgroundProcessing = true;
- }
- public void FireBackgroundProcessing()
- {
- if(!AskedForBackgroundProcessing)
- return;
- AskedForBackgroundProcessing = false;
- ReadyForBackgroundProcessing?.Invoke();
- }
- }
- class SimpleControlledDispatcherImpl : SimpleDispatcherWithBackgroundProcessingImpl, IControlledDispatcherImpl
- {
- private readonly bool _useTestTimeout = true;
- private readonly CancellationToken? _cancel;
- public int RunLoopCount { get; private set; }
- public SimpleControlledDispatcherImpl()
- {
- }
- public SimpleControlledDispatcherImpl(CancellationToken cancel, bool useTestTimeout = false)
- {
- _useTestTimeout = useTestTimeout;
- _cancel = cancel;
- }
- public void RunLoop(CancellationToken token)
- {
- RunLoopCount++;
- var st = Stopwatch.StartNew();
- while (!token.IsCancellationRequested || _cancel?.IsCancellationRequested == true)
- {
- FireBackgroundProcessing();
- ExecuteSignal();
- if (_useTestTimeout)
- Assert.True(st.ElapsedMilliseconds < 4000, "RunLoop exceeded test time quota");
- else
- Thread.Sleep(10);
- }
- }
- }
- [Fact]
- public void DispatcherExecutesJobsAccordingToPriority()
- {
- var impl = new SimpleDispatcherImpl();
- var disp = new Dispatcher(impl);
- var actions = new List<string>();
- disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background);
- disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render);
- disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input);
- Assert.True(impl.AskedForSignal);
- impl.ExecuteSignal();
- Assert.Equal(new[] { "Render", "Input", "Background" }, actions);
- }
- [Fact]
- public void DispatcherPreservesOrderWhenChangingPriority()
- {
- var impl = new SimpleDispatcherImpl();
- var disp = new Dispatcher(impl);
- var actions = new List<string>();
- var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background);
- var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input);
- disp.Post(() => actions.Add("Render"), DispatcherPriority.Render);
- toPromote.Priority = DispatcherPriority.Render;
- toPromote2.Priority = DispatcherPriority.Render;
- Assert.True(impl.AskedForSignal);
- impl.ExecuteSignal();
- Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions);
- }
- [Fact]
- public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached()
- {
- var impl = new SimpleDispatcherImpl();
- var disp = new Dispatcher(impl);
- var actions = new List<int>();
- for (var c = 0; c < 10; c++)
- {
- var itemId = c;
- disp.Post(() =>
- {
- actions.Add(itemId);
- impl.Now += 20;
- }, DispatcherPriority.Background);
- }
- Assert.False(impl.AskedForSignal);
- Assert.NotNull(impl.NextTimer);
- for (var c = 0; c < 4; c++)
- {
- Assert.NotNull(impl.NextTimer);
- Assert.False(impl.AskedForSignal);
- impl.ExecuteTimer();
- Assert.False(impl.AskedForSignal);
- impl.ExecuteSignal();
- var expectedCount = (c + 1) * 3;
- if (c == 3)
- expectedCount = 10;
- Assert.Equal(Enumerable.Range(0, expectedCount), actions);
- Assert.False(impl.AskedForSignal);
- if (c < 3)
- {
- Assert.True(impl.NextTimer > impl.Now);
- }
- else
- Assert.Null(impl.NextTimer);
- }
- }
- [Fact]
- public void DispatcherStopsItemProcessingWhenInputIsPending()
- {
- var impl = new SimpleDispatcherImpl();
- impl.TestInputPending = true;
- var disp = new Dispatcher(impl);
- var actions = new List<int>();
- for (var c = 0; c < 10; c++)
- {
- var itemId = c;
- disp.Post(() =>
- {
- actions.Add(itemId);
- if (itemId == 0 || itemId == 3 || itemId == 7)
- impl.TestInputPending = true;
- }, DispatcherPriority.Background);
- }
- Assert.False(impl.AskedForSignal);
- Assert.NotNull(impl.NextTimer);
- impl.TestInputPending = false;
- for (var c = 0; c < 4; c++)
- {
- Assert.NotNull(impl.NextTimer);
- impl.ExecuteTimer();
- Assert.False(impl.AskedForSignal);
- var expectedCount = c switch
- {
- 0 => 1,
- 1 => 4,
- 2 => 8,
- 3 => 10,
- _ => throw new InvalidOperationException($"Unexpected value {c}")
- };
- Assert.Equal(Enumerable.Range(0, expectedCount), actions);
- Assert.False(impl.AskedForSignal);
- if (c < 3)
- {
- Assert.True(impl.NextTimer > impl.Now);
- impl.Now = impl.NextTimer.Value + 1;
- }
- else
- Assert.Null(impl.NextTimer);
- impl.TestInputPending = false;
- }
- }
- [Theory,
- InlineData(false, false),
- InlineData(false, true),
- InlineData(true, false),
- InlineData(true, true)]
- public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground)
- {
- var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl();
- var disp = new Dispatcher(impl);
- bool finished = false;
- disp.InvokeAsync(() => finished = true,
- foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait();
- Assert.True(finished);
- if (controlled)
- Assert.Equal(foreground ? 0 : 1, ((SimpleControlledDispatcherImpl)impl).RunLoopCount);
- }
- class DispatcherServices : IDisposable
- {
- private readonly IDisposable _scope;
- public DispatcherServices(IDispatcherImpl impl)
- {
- _scope = AvaloniaLocator.EnterScope();
- AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(impl);
- Dispatcher.ResetForUnitTests();
- SynchronizationContext.SetSynchronizationContext(null);
- }
- public void Dispose()
- {
- Dispatcher.ResetForUnitTests();
- _scope.Dispose();
- SynchronizationContext.SetSynchronizationContext(null);
- }
- }
- [Fact]
- public void ExitAllFramesShouldExitAllFramesAndBeAbleToContinue()
- {
- using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
- {
- var actions = new List<string>();
- var disp = Dispatcher.UIThread;
- disp.Post(() =>
- {
- actions.Add("Nested frame");
- Dispatcher.UIThread.MainLoop(CancellationToken.None);
- actions.Add("Nested frame exited");
- });
- disp.Post(() =>
- {
- actions.Add("ExitAllFrames");
- disp.ExitAllFrames();
- });
- disp.MainLoop(CancellationToken.None);
- Assert.Equal(new[] { "Nested frame", "ExitAllFrames", "Nested frame exited" }, actions);
- actions.Clear();
- var secondLoop = new CancellationTokenSource();
- disp.Post(() =>
- {
- actions.Add("Callback after exit");
- secondLoop.Cancel();
- });
- disp.MainLoop(secondLoop.Token);
- Assert.Equal(new[] { "Callback after exit" }, actions);
- }
- }
- [Fact]
- public void ShutdownShouldExitAllFramesAndNotAllowNewFrames()
- {
- using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
- {
- var actions = new List<string>();
- var disp = Dispatcher.UIThread;
- disp.Post(() =>
- {
- actions.Add("Nested frame");
- Dispatcher.UIThread.MainLoop(CancellationToken.None);
- actions.Add("Nested frame exited");
- });
- disp.Post(() =>
- {
- actions.Add("Shutdown");
- disp.BeginInvokeShutdown(DispatcherPriority.Normal);
- });
- disp.Post(() =>
- {
- actions.Add("Nested frame after shutdown");
- // This should exit immediately and not run any jobs
- Dispatcher.UIThread.MainLoop(CancellationToken.None);
- actions.Add("Nested frame after shutdown exited");
- });
- var criticalFrameAfterShutdown = new DispatcherFrame(false);
- disp.Post(() =>
- {
- actions.Add("Critical frame after shutdown");
- Dispatcher.UIThread.PushFrame(criticalFrameAfterShutdown);
- actions.Add("Critical frame after shutdown exited");
- });
- disp.Post(() =>
- {
- actions.Add("Stop critical frame");
- criticalFrameAfterShutdown.Continue = false;
- });
- disp.MainLoop(CancellationToken.None);
- Assert.Equal(new[]
- {
- "Nested frame",
- "Shutdown",
- // Normal nested frames are supposed to exit immediately
- "Nested frame after shutdown", "Nested frame after shutdown exited",
- // if frame is configured to not answer dispatcher requests, it should be allowed to run
- "Critical frame after shutdown", "Stop critical frame", "Critical frame after shutdown exited",
- // After 3-rd level frames have exited, the normal nested frame exits too
- "Nested frame exited"
- }, actions);
- actions.Clear();
- disp.Post(()=>actions.Add("Frame after shutdown finished"));
- Assert.Throws<InvalidOperationException>(() => disp.MainLoop(CancellationToken.None));
- Assert.Empty(actions);
- }
- }
- class WaitHelper : SynchronizationContext, NonPumpingLockHelper.IHelperImpl
- {
- public int WaitCount;
- public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
- {
- WaitCount++;
- return base.Wait(waitHandles, waitAll, millisecondsTimeout);
- }
- }
- [Fact]
- public void DisableProcessingShouldStopProcessing()
- {
- using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
- {
- var helper = new WaitHelper();
- AvaloniaLocator.CurrentMutable.Bind<NonPumpingLockHelper.IHelperImpl>().ToConstant(helper);
- using (Dispatcher.UIThread.DisableProcessing())
- {
- Assert.True(SynchronizationContext.Current is NonPumpingSyncContext);
- Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.MainLoop(CancellationToken.None));
- Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.RunJobs());
- }
- var avaloniaContext = new AvaloniaSynchronizationContext(Dispatcher.UIThread, DispatcherPriority.Default, true);
- SynchronizationContext.SetSynchronizationContext(avaloniaContext);
- var waitHandle = new ManualResetEvent(true);
- helper.WaitCount = 0;
- waitHandle.WaitOne(100);
- Assert.Equal(0, helper.WaitCount);
- using (Dispatcher.UIThread.DisableProcessing())
- {
- Assert.Equal(avaloniaContext, SynchronizationContext.Current);
- waitHandle.WaitOne(100);
- Assert.Equal(1, helper.WaitCount);
- }
- }
- }
- [Fact]
- public void DispatcherOperationsHaveContextWithProperPriority()
- {
- using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
- {
- SynchronizationContext.SetSynchronizationContext(null);
- var disp = Dispatcher.UIThread;
- var priorities = new List<DispatcherPriority>();
- void DumpCurrentPriority() =>
- priorities.Add(((AvaloniaSynchronizationContext)SynchronizationContext.Current!).Priority);
- disp.Post(DumpCurrentPriority, DispatcherPriority.Normal);
- disp.Post(DumpCurrentPriority, DispatcherPriority.Loaded);
- disp.Post(DumpCurrentPriority, DispatcherPriority.Input);
- disp.Post(() =>
- {
- DumpCurrentPriority();
- disp.ExitAllFrames();
- }, DispatcherPriority.Background);
- disp.MainLoop(CancellationToken.None);
- disp.Invoke(DumpCurrentPriority, DispatcherPriority.Send);
- disp.Invoke(() =>
- {
- DumpCurrentPriority();
- return 1;
- }, DispatcherPriority.Send);
- Assert.Equal(
- new[]
- {
- DispatcherPriority.Normal, DispatcherPriority.Loaded, DispatcherPriority.Input, DispatcherPriority.Background,
- DispatcherPriority.Send, DispatcherPriority.Send,
- },
- priorities);
- }
- }
- [Fact]
- [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Tests the dispatcher itself")]
- public void DispatcherInvokeAsyncUnwrapsTasks()
- {
- int asyncMethodStage = 0;
- async Task AsyncMethod()
- {
- asyncMethodStage = 1;
- await Task.Delay(200);
- asyncMethodStage = 2;
- }
- async Task<int> AsyncMethodWithResult()
- {
- await Task.Delay(100);
- return 1;
- }
- async Task Test()
- {
- await Dispatcher.UIThread.InvokeAsync(AsyncMethod);
- Assert.Equal(2, asyncMethodStage);
- Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult));
- asyncMethodStage = 0;
- await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default);
- Assert.Equal(2, asyncMethodStage);
- Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default));
- Dispatcher.UIThread.ExitAllFrames();
- }
- using (new DispatcherServices(new ManagedDispatcherImpl(null)))
- {
- var t = Test();
- var cts = new CancellationTokenSource();
- Task.Delay(3000).ContinueWith(_ => cts.Cancel());
- Dispatcher.UIThread.MainLoop(cts.Token);
- Assert.True(t.IsCompletedSuccessfully);
- t.GetAwaiter().GetResult();
- }
- }
- [Fact]
- public async Task DispatcherResumeContinuesOnUIThread()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- var workload = Dispatcher.UIThread.InvokeAsync(
- async () =>
- {
- Assert.True(Dispatcher.UIThread.CheckAccess());
- await Task.Delay(1).ConfigureAwait(false);
- Assert.False(Dispatcher.UIThread.CheckAccess());
- await Dispatcher.UIThread.Resume();
- Assert.True(Dispatcher.UIThread.CheckAccess());
- tokenSource.Cancel();
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- }
- [Fact]
- public async Task DispatcherYieldContinuesOnUIThread()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- var workload = Dispatcher.UIThread.InvokeAsync(
- async () =>
- {
- Assert.True(Dispatcher.UIThread.CheckAccess());
- await Dispatcher.Yield();
- Assert.True(Dispatcher.UIThread.CheckAccess());
- tokenSource.Cancel();
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- }
- [Fact]
- public async Task DispatcherYieldThrowsOnNonUIThread()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- var workload = Dispatcher.UIThread.InvokeAsync(
- async () =>
- {
- Assert.True(Dispatcher.UIThread.CheckAccess());
- await Task.Delay(1).ConfigureAwait(false);
- Assert.False(Dispatcher.UIThread.CheckAccess());
- await Assert.ThrowsAsync<InvalidOperationException>(async () => await Dispatcher.Yield());
- tokenSource.Cancel();
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- }
- [Fact]
- public async Task AwaitWithPriorityRunsOnUIThread()
- {
- static async Task<int> Workload()
- {
- await Task.Delay(1).ConfigureAwait(false);
- Assert.False(Dispatcher.UIThread.CheckAccess());
- return Thread.CurrentThread.ManagedThreadId;
- }
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- var workload = Dispatcher.UIThread.InvokeAsync(
- async () =>
- {
- Assert.True(Dispatcher.UIThread.CheckAccess());
- Task taskWithoutResult = Workload();
- await Dispatcher.UIThread.AwaitWithPriority(taskWithoutResult, DispatcherPriority.Default);
- Assert.True(Dispatcher.UIThread.CheckAccess());
- Task<int> taskWithResult = Workload();
- await Dispatcher.UIThread.AwaitWithPriority(taskWithResult, DispatcherPriority.Default);
- Assert.True(Dispatcher.UIThread.CheckAccess());
- tokenSource.Cancel();
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- }
- #nullable enable
- private class AsyncLocalTestClass
- {
- public AsyncLocal<string?> AsyncLocalField { get; set; } = new AsyncLocal<string?>();
- }
- [Fact]
- public async Task ExecutionContextIsPreservedInDispatcherInvokeAsync()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- string? test1 = null;
- string? test2 = null;
- string? test3 = null;
- // All test code must run inside Task.Run to avoid interfering with the test:
- // 1. Prevent the execution context from being captured by MainLoop.
- // 2. Prevent the execution context from remaining effective when set on the same thread.
- var task = Task.Run(() =>
- {
- var testObject = new AsyncLocalTestClass();
- // Test 1: Verify Task.Run preserves the execution context.
- // First, test Task.Run to ensure that the preceding validation always passes, serving as a baseline for the subsequent Invoke/InvokeAsync tests.
- // This way, if a later test fails, we have the .NET framework's baseline behavior for reference.
- testObject.AsyncLocalField.Value = "Initial Value";
- var task1 = Task.Run(() =>
- {
- test1 = testObject.AsyncLocalField.Value;
- });
- // Test 2: Verify Invoke preserves the execution context.
- testObject.AsyncLocalField.Value = "Initial Value";
- Dispatcher.UIThread.Invoke(() =>
- {
- test2 = testObject.AsyncLocalField.Value;
- });
- // Test 3: Verify InvokeAsync preserves the execution context.
- testObject.AsyncLocalField.Value = "Initial Value";
- _ = Dispatcher.UIThread.InvokeAsync(() =>
- {
- test3 = testObject.AsyncLocalField.Value;
- });
- _ = Dispatcher.UIThread.InvokeAsync(async () =>
- {
- await Task.WhenAll(task1);
- tokenSource.Cancel();
- });
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- await Task.WhenAll(task);
- // Assertions
- // Task.Run: Always passes (guaranteed by the .NET runtime).
- Assert.Equal("Initial Value", test1);
- // Invoke: Always passes because the context is not changed.
- Assert.Equal("Initial Value", test2);
- // InvokeAsync: See https://github.com/AvaloniaUI/Avalonia/pull/19163
- Assert.Equal("Initial Value", test3);
- }
- [Fact]
- public async Task ExecutionContextIsNotPreservedAmongDispatcherInvokeAsync()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- string? test = null;
- // All test code must run inside Task.Run to avoid interfering with the test:
- // 1. Prevent the execution context from being captured by MainLoop.
- // 2. Prevent the execution context from remaining effective when set on the same thread.
- var task = Task.Run(() =>
- {
- var testObject = new AsyncLocalTestClass();
- // Test: Verify that InvokeAsync calls do not share execution context between each other.
- _ = Dispatcher.UIThread.InvokeAsync(() =>
- {
- testObject.AsyncLocalField.Value = "Initial Value";
- });
- _ = Dispatcher.UIThread.InvokeAsync(() =>
- {
- test = testObject.AsyncLocalField.Value;
- });
- _ = Dispatcher.UIThread.InvokeAsync(() =>
- {
- tokenSource.Cancel();
- });
- });
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- await Task.WhenAll(task);
- // Assertions
- // The value should NOT flow between different InvokeAsync execution contexts.
- Assert.Null(test);
- }
- [Fact]
- public async Task ExecutionContextCultureInfoIsPreservedInDispatcherInvokeAsync()
- {
- using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
- var tokenSource = new CancellationTokenSource();
- string? test1 = null;
- string? test2 = null;
- string? test3 = null;
- var oldCulture = Thread.CurrentThread.CurrentCulture;
- // All test code must run inside Task.Run to avoid interfering with the test:
- // 1. Prevent the execution context from being captured by MainLoop.
- // 2. Prevent the execution context from remaining effective when set on the same thread.
- var task = Task.Run(() =>
- {
- // This culture tag is Sumerian and is extremely unlikely to be set as the default on any device,
- // ensuring that this test will not be affected by the user's environment.
- Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("sux-Shaw-UM");
- // Test 1: Verify Task.Run preserves the culture in the execution context.
- // First, test Task.Run to ensure that the preceding validation always passes, serving as a baseline for the subsequent Invoke/InvokeAsync tests.
- // This way, if a later test fails, we have the .NET framework's baseline behavior for reference.
- var task1 = Task.Run(() =>
- {
- test1 = Thread.CurrentThread.CurrentCulture.Name;
- });
- // Test 2: Verify Invoke preserves the execution context.
- Dispatcher.UIThread.Invoke(() =>
- {
- test2 = Thread.CurrentThread.CurrentCulture.Name;
- });
- // Test 3: Verify InvokeAsync preserves the culture in the execution context.
- _ = Dispatcher.UIThread.InvokeAsync(() =>
- {
- test3 = Thread.CurrentThread.CurrentCulture.Name;
- });
- _ = Dispatcher.UIThread.InvokeAsync(async () =>
- {
- await Task.WhenAll(task1);
- tokenSource.Cancel();
- });
- });
- try
- {
- Dispatcher.UIThread.MainLoop(tokenSource.Token);
- await Task.WhenAll(task);
- // Assertions
- // Task.Run: Always passes (guaranteed by the .NET runtime).
- Assert.Equal("sux-Shaw-UM", test1);
- // Invoke: Always passes because the context is not changed.
- Assert.Equal("sux-Shaw-UM", test2);
- // InvokeAsync: See https://github.com/AvaloniaUI/Avalonia/pull/19163
- Assert.Equal("sux-Shaw-UM", test3);
- }
- finally
- {
- Thread.CurrentThread.CurrentCulture = oldCulture;
- // Ensure that this test does not have a negative impact on other tests.
- Assert.NotEqual("sux-Shaw-UM", oldCulture.Name);
- }
- }
- #nullable restore
- }
|