DispatcherTests.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Avalonia.Controls.Platform;
  10. using Avalonia.Threading;
  11. using Avalonia.Utilities;
  12. using Xunit;
  13. namespace Avalonia.Base.UnitTests;
  14. public partial class DispatcherTests
  15. {
  16. class SimpleDispatcherImpl : IDispatcherImpl, IDispatcherImplWithPendingInput
  17. {
  18. private readonly Thread _loopThread = Thread.CurrentThread;
  19. private readonly object _lock = new();
  20. public bool CurrentThreadIsLoopThread => Thread.CurrentThread == _loopThread;
  21. public void Signal()
  22. {
  23. lock (_lock)
  24. AskedForSignal = true;
  25. }
  26. public event Action Signaled;
  27. public event Action Timer;
  28. public long? NextTimer { get; private set; }
  29. public bool AskedForSignal { get; private set; }
  30. public void UpdateTimer(long? dueTimeInTicks)
  31. {
  32. NextTimer = dueTimeInTicks;
  33. }
  34. public long Now { get; set; }
  35. public void ExecuteSignal()
  36. {
  37. lock (_lock)
  38. {
  39. if (!AskedForSignal)
  40. return;
  41. AskedForSignal = false;
  42. }
  43. Signaled?.Invoke();
  44. }
  45. public void ExecuteTimer()
  46. {
  47. if (NextTimer == null)
  48. return;
  49. Now = NextTimer.Value;
  50. Timer?.Invoke();
  51. }
  52. public bool CanQueryPendingInput => TestInputPending != null;
  53. public bool HasPendingInput => TestInputPending == true;
  54. public bool? TestInputPending { get; set; }
  55. }
  56. class SimpleDispatcherWithBackgroundProcessingImpl : SimpleDispatcherImpl, IDispatcherImplWithExplicitBackgroundProcessing
  57. {
  58. public bool AskedForBackgroundProcessing { get; private set; }
  59. public event Action ReadyForBackgroundProcessing;
  60. public void RequestBackgroundProcessing()
  61. {
  62. if (!CurrentThreadIsLoopThread)
  63. throw new InvalidOperationException();
  64. AskedForBackgroundProcessing = true;
  65. }
  66. public void FireBackgroundProcessing()
  67. {
  68. if(!AskedForBackgroundProcessing)
  69. return;
  70. AskedForBackgroundProcessing = false;
  71. ReadyForBackgroundProcessing?.Invoke();
  72. }
  73. }
  74. class SimpleControlledDispatcherImpl : SimpleDispatcherWithBackgroundProcessingImpl, IControlledDispatcherImpl
  75. {
  76. private readonly bool _useTestTimeout = true;
  77. private readonly CancellationToken? _cancel;
  78. public int RunLoopCount { get; private set; }
  79. public SimpleControlledDispatcherImpl()
  80. {
  81. }
  82. public SimpleControlledDispatcherImpl(CancellationToken cancel, bool useTestTimeout = false)
  83. {
  84. _useTestTimeout = useTestTimeout;
  85. _cancel = cancel;
  86. }
  87. public void RunLoop(CancellationToken token)
  88. {
  89. RunLoopCount++;
  90. var st = Stopwatch.StartNew();
  91. while (!token.IsCancellationRequested || _cancel?.IsCancellationRequested == true)
  92. {
  93. FireBackgroundProcessing();
  94. ExecuteSignal();
  95. if (_useTestTimeout)
  96. Assert.True(st.ElapsedMilliseconds < 4000, "RunLoop exceeded test time quota");
  97. else
  98. Thread.Sleep(10);
  99. }
  100. }
  101. }
  102. [Fact]
  103. public void DispatcherExecutesJobsAccordingToPriority()
  104. {
  105. var impl = new SimpleDispatcherImpl();
  106. var disp = new Dispatcher(impl);
  107. var actions = new List<string>();
  108. disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background);
  109. disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render);
  110. disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input);
  111. Assert.True(impl.AskedForSignal);
  112. impl.ExecuteSignal();
  113. Assert.Equal(new[] { "Render", "Input", "Background" }, actions);
  114. }
  115. [Fact]
  116. public void DispatcherPreservesOrderWhenChangingPriority()
  117. {
  118. var impl = new SimpleDispatcherImpl();
  119. var disp = new Dispatcher(impl);
  120. var actions = new List<string>();
  121. var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background);
  122. var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input);
  123. disp.Post(() => actions.Add("Render"), DispatcherPriority.Render);
  124. toPromote.Priority = DispatcherPriority.Render;
  125. toPromote2.Priority = DispatcherPriority.Render;
  126. Assert.True(impl.AskedForSignal);
  127. impl.ExecuteSignal();
  128. Assert.Equal(new[] { "PromotedRender", "PromotedRender2", "Render" }, actions);
  129. }
  130. [Fact]
  131. public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached()
  132. {
  133. var impl = new SimpleDispatcherImpl();
  134. var disp = new Dispatcher(impl);
  135. var actions = new List<int>();
  136. for (var c = 0; c < 10; c++)
  137. {
  138. var itemId = c;
  139. disp.Post(() =>
  140. {
  141. actions.Add(itemId);
  142. impl.Now += 20;
  143. }, DispatcherPriority.Background);
  144. }
  145. Assert.False(impl.AskedForSignal);
  146. Assert.NotNull(impl.NextTimer);
  147. for (var c = 0; c < 4; c++)
  148. {
  149. Assert.NotNull(impl.NextTimer);
  150. Assert.False(impl.AskedForSignal);
  151. impl.ExecuteTimer();
  152. Assert.False(impl.AskedForSignal);
  153. impl.ExecuteSignal();
  154. var expectedCount = (c + 1) * 3;
  155. if (c == 3)
  156. expectedCount = 10;
  157. Assert.Equal(Enumerable.Range(0, expectedCount), actions);
  158. Assert.False(impl.AskedForSignal);
  159. if (c < 3)
  160. {
  161. Assert.True(impl.NextTimer > impl.Now);
  162. }
  163. else
  164. Assert.Null(impl.NextTimer);
  165. }
  166. }
  167. [Fact]
  168. public void DispatcherStopsItemProcessingWhenInputIsPending()
  169. {
  170. var impl = new SimpleDispatcherImpl();
  171. impl.TestInputPending = true;
  172. var disp = new Dispatcher(impl);
  173. var actions = new List<int>();
  174. for (var c = 0; c < 10; c++)
  175. {
  176. var itemId = c;
  177. disp.Post(() =>
  178. {
  179. actions.Add(itemId);
  180. if (itemId == 0 || itemId == 3 || itemId == 7)
  181. impl.TestInputPending = true;
  182. }, DispatcherPriority.Background);
  183. }
  184. Assert.False(impl.AskedForSignal);
  185. Assert.NotNull(impl.NextTimer);
  186. impl.TestInputPending = false;
  187. for (var c = 0; c < 4; c++)
  188. {
  189. Assert.NotNull(impl.NextTimer);
  190. impl.ExecuteTimer();
  191. Assert.False(impl.AskedForSignal);
  192. var expectedCount = c switch
  193. {
  194. 0 => 1,
  195. 1 => 4,
  196. 2 => 8,
  197. 3 => 10,
  198. _ => throw new InvalidOperationException($"Unexpected value {c}")
  199. };
  200. Assert.Equal(Enumerable.Range(0, expectedCount), actions);
  201. Assert.False(impl.AskedForSignal);
  202. if (c < 3)
  203. {
  204. Assert.True(impl.NextTimer > impl.Now);
  205. impl.Now = impl.NextTimer.Value + 1;
  206. }
  207. else
  208. Assert.Null(impl.NextTimer);
  209. impl.TestInputPending = false;
  210. }
  211. }
  212. [Theory,
  213. InlineData(false, false),
  214. InlineData(false, true),
  215. InlineData(true, false),
  216. InlineData(true, true)]
  217. public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground)
  218. {
  219. var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl();
  220. var disp = new Dispatcher(impl);
  221. bool finished = false;
  222. disp.InvokeAsync(() => finished = true,
  223. foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait();
  224. Assert.True(finished);
  225. if (controlled)
  226. Assert.Equal(foreground ? 0 : 1, ((SimpleControlledDispatcherImpl)impl).RunLoopCount);
  227. }
  228. class DispatcherServices : IDisposable
  229. {
  230. private readonly IDisposable _scope;
  231. public DispatcherServices(IDispatcherImpl impl)
  232. {
  233. _scope = AvaloniaLocator.EnterScope();
  234. AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(impl);
  235. Dispatcher.ResetForUnitTests();
  236. SynchronizationContext.SetSynchronizationContext(null);
  237. }
  238. public void Dispose()
  239. {
  240. Dispatcher.ResetForUnitTests();
  241. _scope.Dispose();
  242. SynchronizationContext.SetSynchronizationContext(null);
  243. }
  244. }
  245. [Fact]
  246. public void ExitAllFramesShouldExitAllFramesAndBeAbleToContinue()
  247. {
  248. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  249. {
  250. var actions = new List<string>();
  251. var disp = Dispatcher.UIThread;
  252. disp.Post(() =>
  253. {
  254. actions.Add("Nested frame");
  255. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  256. actions.Add("Nested frame exited");
  257. });
  258. disp.Post(() =>
  259. {
  260. actions.Add("ExitAllFrames");
  261. disp.ExitAllFrames();
  262. });
  263. disp.MainLoop(CancellationToken.None);
  264. Assert.Equal(new[] { "Nested frame", "ExitAllFrames", "Nested frame exited" }, actions);
  265. actions.Clear();
  266. var secondLoop = new CancellationTokenSource();
  267. disp.Post(() =>
  268. {
  269. actions.Add("Callback after exit");
  270. secondLoop.Cancel();
  271. });
  272. disp.MainLoop(secondLoop.Token);
  273. Assert.Equal(new[] { "Callback after exit" }, actions);
  274. }
  275. }
  276. [Fact]
  277. public void ShutdownShouldExitAllFramesAndNotAllowNewFrames()
  278. {
  279. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  280. {
  281. var actions = new List<string>();
  282. var disp = Dispatcher.UIThread;
  283. disp.Post(() =>
  284. {
  285. actions.Add("Nested frame");
  286. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  287. actions.Add("Nested frame exited");
  288. });
  289. disp.Post(() =>
  290. {
  291. actions.Add("Shutdown");
  292. disp.BeginInvokeShutdown(DispatcherPriority.Normal);
  293. });
  294. disp.Post(() =>
  295. {
  296. actions.Add("Nested frame after shutdown");
  297. // This should exit immediately and not run any jobs
  298. Dispatcher.UIThread.MainLoop(CancellationToken.None);
  299. actions.Add("Nested frame after shutdown exited");
  300. });
  301. var criticalFrameAfterShutdown = new DispatcherFrame(false);
  302. disp.Post(() =>
  303. {
  304. actions.Add("Critical frame after shutdown");
  305. Dispatcher.UIThread.PushFrame(criticalFrameAfterShutdown);
  306. actions.Add("Critical frame after shutdown exited");
  307. });
  308. disp.Post(() =>
  309. {
  310. actions.Add("Stop critical frame");
  311. criticalFrameAfterShutdown.Continue = false;
  312. });
  313. disp.MainLoop(CancellationToken.None);
  314. Assert.Equal(new[]
  315. {
  316. "Nested frame",
  317. "Shutdown",
  318. // Normal nested frames are supposed to exit immediately
  319. "Nested frame after shutdown", "Nested frame after shutdown exited",
  320. // if frame is configured to not answer dispatcher requests, it should be allowed to run
  321. "Critical frame after shutdown", "Stop critical frame", "Critical frame after shutdown exited",
  322. // After 3-rd level frames have exited, the normal nested frame exits too
  323. "Nested frame exited"
  324. }, actions);
  325. actions.Clear();
  326. disp.Post(()=>actions.Add("Frame after shutdown finished"));
  327. Assert.Throws<InvalidOperationException>(() => disp.MainLoop(CancellationToken.None));
  328. Assert.Empty(actions);
  329. }
  330. }
  331. class WaitHelper : SynchronizationContext, NonPumpingLockHelper.IHelperImpl
  332. {
  333. public int WaitCount;
  334. public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
  335. {
  336. WaitCount++;
  337. return base.Wait(waitHandles, waitAll, millisecondsTimeout);
  338. }
  339. }
  340. [Fact]
  341. public void DisableProcessingShouldStopProcessing()
  342. {
  343. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  344. {
  345. var helper = new WaitHelper();
  346. AvaloniaLocator.CurrentMutable.Bind<NonPumpingLockHelper.IHelperImpl>().ToConstant(helper);
  347. using (Dispatcher.UIThread.DisableProcessing())
  348. {
  349. Assert.True(SynchronizationContext.Current is NonPumpingSyncContext);
  350. Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.MainLoop(CancellationToken.None));
  351. Assert.Throws<InvalidOperationException>(() => Dispatcher.UIThread.RunJobs());
  352. }
  353. var avaloniaContext = new AvaloniaSynchronizationContext(Dispatcher.UIThread, DispatcherPriority.Default, true);
  354. SynchronizationContext.SetSynchronizationContext(avaloniaContext);
  355. var waitHandle = new ManualResetEvent(true);
  356. helper.WaitCount = 0;
  357. waitHandle.WaitOne(100);
  358. Assert.Equal(0, helper.WaitCount);
  359. using (Dispatcher.UIThread.DisableProcessing())
  360. {
  361. Assert.Equal(avaloniaContext, SynchronizationContext.Current);
  362. waitHandle.WaitOne(100);
  363. Assert.Equal(1, helper.WaitCount);
  364. }
  365. }
  366. }
  367. [Fact]
  368. public void DispatcherOperationsHaveContextWithProperPriority()
  369. {
  370. using (new DispatcherServices(new SimpleControlledDispatcherImpl()))
  371. {
  372. SynchronizationContext.SetSynchronizationContext(null);
  373. var disp = Dispatcher.UIThread;
  374. var priorities = new List<DispatcherPriority>();
  375. void DumpCurrentPriority() =>
  376. priorities.Add(((AvaloniaSynchronizationContext)SynchronizationContext.Current!).Priority);
  377. disp.Post(DumpCurrentPriority, DispatcherPriority.Normal);
  378. disp.Post(DumpCurrentPriority, DispatcherPriority.Loaded);
  379. disp.Post(DumpCurrentPriority, DispatcherPriority.Input);
  380. disp.Post(() =>
  381. {
  382. DumpCurrentPriority();
  383. disp.ExitAllFrames();
  384. }, DispatcherPriority.Background);
  385. disp.MainLoop(CancellationToken.None);
  386. disp.Invoke(DumpCurrentPriority, DispatcherPriority.Send);
  387. disp.Invoke(() =>
  388. {
  389. DumpCurrentPriority();
  390. return 1;
  391. }, DispatcherPriority.Send);
  392. Assert.Equal(
  393. new[]
  394. {
  395. DispatcherPriority.Normal, DispatcherPriority.Loaded, DispatcherPriority.Input, DispatcherPriority.Background,
  396. DispatcherPriority.Send, DispatcherPriority.Send,
  397. },
  398. priorities);
  399. }
  400. }
  401. [Fact]
  402. [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Tests the dispatcher itself")]
  403. public void DispatcherInvokeAsyncUnwrapsTasks()
  404. {
  405. int asyncMethodStage = 0;
  406. async Task AsyncMethod()
  407. {
  408. asyncMethodStage = 1;
  409. await Task.Delay(200);
  410. asyncMethodStage = 2;
  411. }
  412. async Task<int> AsyncMethodWithResult()
  413. {
  414. await Task.Delay(100);
  415. return 1;
  416. }
  417. async Task Test()
  418. {
  419. await Dispatcher.UIThread.InvokeAsync(AsyncMethod);
  420. Assert.Equal(2, asyncMethodStage);
  421. Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult));
  422. asyncMethodStage = 0;
  423. await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default);
  424. Assert.Equal(2, asyncMethodStage);
  425. Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default));
  426. Dispatcher.UIThread.ExitAllFrames();
  427. }
  428. using (new DispatcherServices(new ManagedDispatcherImpl(null)))
  429. {
  430. var t = Test();
  431. var cts = new CancellationTokenSource();
  432. Task.Delay(3000).ContinueWith(_ => cts.Cancel());
  433. Dispatcher.UIThread.MainLoop(cts.Token);
  434. Assert.True(t.IsCompletedSuccessfully);
  435. t.GetAwaiter().GetResult();
  436. }
  437. }
  438. [Fact]
  439. public async Task DispatcherResumeContinuesOnUIThread()
  440. {
  441. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  442. var tokenSource = new CancellationTokenSource();
  443. var workload = Dispatcher.UIThread.InvokeAsync(
  444. async () =>
  445. {
  446. Assert.True(Dispatcher.UIThread.CheckAccess());
  447. await Task.Delay(1).ConfigureAwait(false);
  448. Assert.False(Dispatcher.UIThread.CheckAccess());
  449. await Dispatcher.UIThread.Resume();
  450. Assert.True(Dispatcher.UIThread.CheckAccess());
  451. tokenSource.Cancel();
  452. });
  453. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  454. }
  455. [Fact]
  456. public async Task DispatcherYieldContinuesOnUIThread()
  457. {
  458. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  459. var tokenSource = new CancellationTokenSource();
  460. var workload = Dispatcher.UIThread.InvokeAsync(
  461. async () =>
  462. {
  463. Assert.True(Dispatcher.UIThread.CheckAccess());
  464. await Dispatcher.Yield();
  465. Assert.True(Dispatcher.UIThread.CheckAccess());
  466. tokenSource.Cancel();
  467. });
  468. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  469. }
  470. [Fact]
  471. public async Task DispatcherYieldThrowsOnNonUIThread()
  472. {
  473. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  474. var tokenSource = new CancellationTokenSource();
  475. var workload = Dispatcher.UIThread.InvokeAsync(
  476. async () =>
  477. {
  478. Assert.True(Dispatcher.UIThread.CheckAccess());
  479. await Task.Delay(1).ConfigureAwait(false);
  480. Assert.False(Dispatcher.UIThread.CheckAccess());
  481. await Assert.ThrowsAsync<InvalidOperationException>(async () => await Dispatcher.Yield());
  482. tokenSource.Cancel();
  483. });
  484. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  485. }
  486. [Fact]
  487. public async Task AwaitWithPriorityRunsOnUIThread()
  488. {
  489. static async Task<int> Workload()
  490. {
  491. await Task.Delay(1).ConfigureAwait(false);
  492. Assert.False(Dispatcher.UIThread.CheckAccess());
  493. return Thread.CurrentThread.ManagedThreadId;
  494. }
  495. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  496. var tokenSource = new CancellationTokenSource();
  497. var workload = Dispatcher.UIThread.InvokeAsync(
  498. async () =>
  499. {
  500. Assert.True(Dispatcher.UIThread.CheckAccess());
  501. Task taskWithoutResult = Workload();
  502. await Dispatcher.UIThread.AwaitWithPriority(taskWithoutResult, DispatcherPriority.Default);
  503. Assert.True(Dispatcher.UIThread.CheckAccess());
  504. Task<int> taskWithResult = Workload();
  505. await Dispatcher.UIThread.AwaitWithPriority(taskWithResult, DispatcherPriority.Default);
  506. Assert.True(Dispatcher.UIThread.CheckAccess());
  507. tokenSource.Cancel();
  508. });
  509. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  510. }
  511. #nullable enable
  512. private class AsyncLocalTestClass
  513. {
  514. public AsyncLocal<string?> AsyncLocalField { get; set; } = new AsyncLocal<string?>();
  515. }
  516. [Fact]
  517. public async Task ExecutionContextIsPreservedInDispatcherInvokeAsync()
  518. {
  519. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  520. var tokenSource = new CancellationTokenSource();
  521. string? test1 = null;
  522. string? test2 = null;
  523. string? test3 = null;
  524. // All test code must run inside Task.Run to avoid interfering with the test:
  525. // 1. Prevent the execution context from being captured by MainLoop.
  526. // 2. Prevent the execution context from remaining effective when set on the same thread.
  527. var task = Task.Run(() =>
  528. {
  529. var testObject = new AsyncLocalTestClass();
  530. // Test 1: Verify Task.Run preserves the execution context.
  531. // First, test Task.Run to ensure that the preceding validation always passes, serving as a baseline for the subsequent Invoke/InvokeAsync tests.
  532. // This way, if a later test fails, we have the .NET framework's baseline behavior for reference.
  533. testObject.AsyncLocalField.Value = "Initial Value";
  534. var task1 = Task.Run(() =>
  535. {
  536. test1 = testObject.AsyncLocalField.Value;
  537. });
  538. // Test 2: Verify Invoke preserves the execution context.
  539. testObject.AsyncLocalField.Value = "Initial Value";
  540. Dispatcher.UIThread.Invoke(() =>
  541. {
  542. test2 = testObject.AsyncLocalField.Value;
  543. });
  544. // Test 3: Verify InvokeAsync preserves the execution context.
  545. testObject.AsyncLocalField.Value = "Initial Value";
  546. _ = Dispatcher.UIThread.InvokeAsync(() =>
  547. {
  548. test3 = testObject.AsyncLocalField.Value;
  549. });
  550. _ = Dispatcher.UIThread.InvokeAsync(async () =>
  551. {
  552. await Task.WhenAll(task1);
  553. tokenSource.Cancel();
  554. });
  555. });
  556. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  557. await Task.WhenAll(task);
  558. // Assertions
  559. // Task.Run: Always passes (guaranteed by the .NET runtime).
  560. Assert.Equal("Initial Value", test1);
  561. // Invoke: Always passes because the context is not changed.
  562. Assert.Equal("Initial Value", test2);
  563. // InvokeAsync: See https://github.com/AvaloniaUI/Avalonia/pull/19163
  564. Assert.Equal("Initial Value", test3);
  565. }
  566. [Fact]
  567. public async Task ExecutionContextIsNotPreservedAmongDispatcherInvokeAsync()
  568. {
  569. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  570. var tokenSource = new CancellationTokenSource();
  571. string? test = null;
  572. // All test code must run inside Task.Run to avoid interfering with the test:
  573. // 1. Prevent the execution context from being captured by MainLoop.
  574. // 2. Prevent the execution context from remaining effective when set on the same thread.
  575. var task = Task.Run(() =>
  576. {
  577. var testObject = new AsyncLocalTestClass();
  578. // Test: Verify that InvokeAsync calls do not share execution context between each other.
  579. _ = Dispatcher.UIThread.InvokeAsync(() =>
  580. {
  581. testObject.AsyncLocalField.Value = "Initial Value";
  582. });
  583. _ = Dispatcher.UIThread.InvokeAsync(() =>
  584. {
  585. test = testObject.AsyncLocalField.Value;
  586. });
  587. _ = Dispatcher.UIThread.InvokeAsync(() =>
  588. {
  589. tokenSource.Cancel();
  590. });
  591. });
  592. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  593. await Task.WhenAll(task);
  594. // Assertions
  595. // The value should NOT flow between different InvokeAsync execution contexts.
  596. Assert.Null(test);
  597. }
  598. [Fact]
  599. public async Task ExecutionContextCultureInfoIsPreservedInDispatcherInvokeAsync()
  600. {
  601. using var services = new DispatcherServices(new SimpleControlledDispatcherImpl());
  602. var tokenSource = new CancellationTokenSource();
  603. string? test1 = null;
  604. string? test2 = null;
  605. string? test3 = null;
  606. var oldCulture = Thread.CurrentThread.CurrentCulture;
  607. // All test code must run inside Task.Run to avoid interfering with the test:
  608. // 1. Prevent the execution context from being captured by MainLoop.
  609. // 2. Prevent the execution context from remaining effective when set on the same thread.
  610. var task = Task.Run(() =>
  611. {
  612. // This culture tag is Sumerian and is extremely unlikely to be set as the default on any device,
  613. // ensuring that this test will not be affected by the user's environment.
  614. Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("sux-Shaw-UM");
  615. // Test 1: Verify Task.Run preserves the culture in the execution context.
  616. // First, test Task.Run to ensure that the preceding validation always passes, serving as a baseline for the subsequent Invoke/InvokeAsync tests.
  617. // This way, if a later test fails, we have the .NET framework's baseline behavior for reference.
  618. var task1 = Task.Run(() =>
  619. {
  620. test1 = Thread.CurrentThread.CurrentCulture.Name;
  621. });
  622. // Test 2: Verify Invoke preserves the execution context.
  623. Dispatcher.UIThread.Invoke(() =>
  624. {
  625. test2 = Thread.CurrentThread.CurrentCulture.Name;
  626. });
  627. // Test 3: Verify InvokeAsync preserves the culture in the execution context.
  628. _ = Dispatcher.UIThread.InvokeAsync(() =>
  629. {
  630. test3 = Thread.CurrentThread.CurrentCulture.Name;
  631. });
  632. _ = Dispatcher.UIThread.InvokeAsync(async () =>
  633. {
  634. await Task.WhenAll(task1);
  635. tokenSource.Cancel();
  636. });
  637. });
  638. try
  639. {
  640. Dispatcher.UIThread.MainLoop(tokenSource.Token);
  641. await Task.WhenAll(task);
  642. // Assertions
  643. // Task.Run: Always passes (guaranteed by the .NET runtime).
  644. Assert.Equal("sux-Shaw-UM", test1);
  645. // Invoke: Always passes because the context is not changed.
  646. Assert.Equal("sux-Shaw-UM", test2);
  647. // InvokeAsync: See https://github.com/AvaloniaUI/Avalonia/pull/19163
  648. Assert.Equal("sux-Shaw-UM", test3);
  649. }
  650. finally
  651. {
  652. Thread.CurrentThread.CurrentCulture = oldCulture;
  653. // Ensure that this test does not have a negative impact on other tests.
  654. Assert.NotEqual("sux-Shaw-UM", oldCulture.Name);
  655. }
  656. }
  657. #nullable restore
  658. }