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.
This commit is contained in:
Lasse R.H. Nielsen
2025-03-26 18:14:04 +01:00
committed by GitHub
parent 8297e44993
commit 9e829cbd92
2 changed files with 99 additions and 3 deletions

View File

@@ -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: <R>(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<ZoneDelegate>(_captureRootZoneDelegate);
}
@override
void scheduleAttachRootWidget(Widget rootWidget) {
// We override the default version of this so that the application-startup widget tree

View File

@@ -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<void>(() async {
final Zone currentZone = Zone.current;
runAsyncZone = currentZone;
// Complete a future when all callbacks have completed.
int pendingCallbacks = 6;
final Completer<void> callbacksDone = Completer<void>();
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', () {