From 9e829cbd922002c2cbd6249c072adb704872059d Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 26 Mar 2025 18:14:04 +0100 Subject: [PATCH] Make `realAsyncZone` run microtasks and timers in the correct zone. (#162731) Current implementation runs timers and microtask callbacks in the root zone. That assumes that the top-level `scheduleMicrotask` or `Timer` constructors have been used, which have so far wrapped the callback with `runCallbackGuarded` before calling the zone implementation. That means that doing `zone.scheduleMicrotask` directly would not ensure that the microtask was run in the correct zone. If a `run` handler throws, it wouldn't be caught. This change makes the `realAsyncZone` do whatever the root zone would do if its `ZoneDelegate` got called with the intended zone and arguments. That should be consistent with the current behavior, and be compatible with incoming bug-fixes to the platform `Zone` behavior. Prepares Flutter for landing https://dart-review.googlesource.com/c/sdk/+/406961 which is currently blocked (so this indirectly fixes https://github.com/dart-lang/sdk/issues/59913). There are no new tests, the goal is that all existing tests keep running, and that they keep doing so when the Dart CL lands. Currently that CL only breaks one test, the `dev/automated_tests/test_smoke_test/fail_test_on_exception_after_test.dart` test which threw the error-after-test in the root zone instead of the test zone. This change fixes that. --- packages/flutter_test/lib/src/binding.dart | 26 ++++++- .../flutter_test/test/widget_tester_test.dart | 76 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 0bddcadb2c..5808af6386 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -1361,7 +1361,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { final Zone realAsyncZone = Zone.current.fork( specification: ZoneSpecification( scheduleMicrotask: (Zone self, ZoneDelegate parent, Zone zone, void Function() f) { - Zone.root.scheduleMicrotask(f); + _rootDelegate.scheduleMicrotask(zone, f); }, createTimer: ( Zone self, @@ -1370,7 +1370,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { Duration duration, void Function() f, ) { - return Zone.root.createTimer(duration, f); + return _rootDelegate.createTimer(zone, duration, f); }, createPeriodicTimer: ( Zone self, @@ -1379,7 +1379,7 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { Duration period, void Function(Timer timer) f, ) { - return Zone.root.createPeriodicTimer(period, f); + return _rootDelegate.createPeriodicTimer(zone, period, f); }, ), ); @@ -1442,6 +1442,26 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _currentFakeAsync!.flushMicrotasks(); } + /// The [ZoneDelegate] for [Zone.root]. + /// + /// Used to schedule (real) microtasks and timers in the root zone, + /// to be run in the correct zone. + static final ZoneDelegate _rootDelegate = _captureRootZoneDelegate(); + + /// Hack to extract the [ZoneDelegate] for [Zone.root]. + static ZoneDelegate _captureRootZoneDelegate() { + final Zone captureZone = Zone.root.fork( + specification: ZoneSpecification( + run: (Zone self, ZoneDelegate parent, Zone zone, R Function() f) { + return parent as R; + }, + ), + ); + // The `_captureRootZoneDelegate` argument just happens to be a constant + // function with the necessary type. It's not called recursively. + return captureZone.run(_captureRootZoneDelegate); + } + @override void scheduleAttachRootWidget(Widget rootWidget) { // We override the default version of this so that the application-startup widget tree diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 206d416d81..297ada1fac 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -406,6 +406,82 @@ void main() { expect(result, isNull); expect(tester.takeException(), isNotNull); }); + + testWidgets('runs in original zone', (WidgetTester tester) async { + final Zone testZone = Zone.current; + Zone? runAsyncZone; + Zone? timerZone; + Zone? periodicTimerZone; + Zone? microtaskZone; + + Zone? innerZone; + Zone? innerTimerZone; + Zone? innerPeriodicTimerZone; + Zone? innerMicrotaskZone; + + await tester.binding.runAsync(() async { + final Zone currentZone = Zone.current; + runAsyncZone = currentZone; + + // Complete a future when all callbacks have completed. + int pendingCallbacks = 6; + final Completer callbacksDone = Completer(); + void onCallback() { + if (--pendingCallbacks == 0) { + testZone.run(() { + callbacksDone.complete(null); + }); + } + } + + // On the runAsync zone itself. + currentZone.createTimer(Duration.zero, () { + timerZone = Zone.current; + onCallback(); + }); + currentZone.createPeriodicTimer(Duration.zero, (Timer timer) { + timer.cancel(); + periodicTimerZone = Zone.current; + onCallback(); + }); + currentZone.scheduleMicrotask(() { + microtaskZone = Zone.current; + onCallback(); + }); + + // On a nested user-created zone. + final Zone inner = runZoned(() => Zone.current); + innerZone = inner; + inner.createTimer(Duration.zero, () { + innerTimerZone = Zone.current; + onCallback(); + }); + inner.createPeriodicTimer(Duration.zero, (Timer timer) { + timer.cancel(); + innerPeriodicTimerZone = Zone.current; + onCallback(); + }); + inner.scheduleMicrotask(() { + innerMicrotaskZone = Zone.current; + onCallback(); + }); + + await callbacksDone.future; + }); + expect(runAsyncZone, isNotNull); + expect(timerZone, same(runAsyncZone)); + expect(periodicTimerZone, same(runAsyncZone)); + expect(microtaskZone, same(runAsyncZone)); + + expect(innerZone, isNotNull); + expect(innerTimerZone, same(innerZone)); + expect(innerPeriodicTimerZone, same(innerZone)); + expect(innerMicrotaskZone, same(innerZone)); + + expect(runAsyncZone, isNot(same(testZone))); + expect(runAsyncZone, isNot(same(innerZone))); + expect(innerZone, isNot(same(testZone))); + }); }); group('showKeyboard', () {