Skip to content

Commit 245d6d4

Browse files
authored
Assert that runApp is called in the same zone as binding.ensureInitialized (#122836)
Assert that runApp is called in the same zone as binding.ensureInitialized
1 parent 9b8f3c8 commit 245d6d4

File tree

14 files changed

+266
-11
lines changed

14 files changed

+266
-11
lines changed

dev/tracing_tests/test/common.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import 'dart:developer' as developer;
66
import 'dart:isolate' as isolate;
77

8+
import 'package:flutter/foundation.dart';
89
import 'package:flutter/scheduler.dart';
10+
import 'package:flutter/widgets.dart';
911
import 'package:flutter_test/flutter_test.dart';
1012
import 'package:vm_service/vm_service.dart';
1113
import 'package:vm_service/vm_service_io.dart';
@@ -52,3 +54,29 @@ Future<void> runFrame(VoidCallback callback) {
5254
callback();
5355
return result;
5456
}
57+
58+
// This binding skips the zones tests. These tests were written before we
59+
// verified zones properly, and they have been legacied-in to avoid having
60+
// to refactor them.
61+
//
62+
// When creating new tests, avoid relying on this class.
63+
class ZoneIgnoringTestBinding extends WidgetsFlutterBinding {
64+
@override
65+
void initInstances() {
66+
super.initInstances();
67+
_instance = this;
68+
}
69+
70+
@override
71+
bool debugCheckZone(String entryPoint) { return true; }
72+
73+
static ZoneIgnoringTestBinding get instance => BindingBase.checkInstance(_instance);
74+
static ZoneIgnoringTestBinding? _instance;
75+
76+
static ZoneIgnoringTestBinding ensureInitialized() {
77+
if (ZoneIgnoringTestBinding._instance == null) {
78+
ZoneIgnoringTestBinding();
79+
}
80+
return ZoneIgnoringTestBinding.instance;
81+
}
82+
}

dev/tracing_tests/test/image_painting_event_test.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import 'package:vm_service/vm_service.dart';
1313
import 'package:vm_service/vm_service_io.dart';
1414

1515
void main() {
16+
LiveTestWidgetsFlutterBinding.ensureInitialized();
17+
1618
late VmService vmService;
1719
late LiveTestWidgetsFlutterBinding binding;
1820
setUpAll(() async {
@@ -27,7 +29,7 @@ void main() {
2729
await vmService.streamListen(EventStreams.kExtension);
2830

2931
// Initialize bindings
30-
binding = LiveTestWidgetsFlutterBinding();
32+
binding = LiveTestWidgetsFlutterBinding.instance;
3133
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
3234
binding.attachRootWidget(const SizedBox.expand());
3335
expect(binding.framesEnabled, true);

dev/tracing_tests/test/inflate_widget_tracing_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final Set<String> interestingLabels = <String>{
1616
};
1717

1818
void main() {
19-
WidgetsFlutterBinding.ensureInitialized();
19+
ZoneIgnoringTestBinding.ensureInitialized();
2020
initTimelineTests();
2121
test('Children of MultiChildRenderObjectElement show up in tracing', () async {
2222
// We don't have expectations around the first frame because there's a race around

dev/tracing_tests/test/inflate_widget_update_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
99
import 'common.dart';
1010

1111
void main() {
12-
WidgetsFlutterBinding.ensureInitialized();
12+
ZoneIgnoringTestBinding.ensureInitialized();
1313
initTimelineTests();
1414
test('Widgets with updated keys produce well formed timelines', () async {
1515
await runFrame(() { runApp(const TestRoot()); });

dev/tracing_tests/test/timeline_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class TestRootState extends State<TestRoot> {
5858
}
5959

6060
void main() {
61-
WidgetsFlutterBinding.ensureInitialized();
61+
ZoneIgnoringTestBinding.ensureInitialized();
6262
initTimelineTests();
6363
test('Timeline', () async {
6464
// We don't have expectations around the first frame because there's a race around

packages/flutter/lib/src/foundation/binding.dart

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:convert' show json;
67
import 'dart:developer' as developer;
78
import 'dart:io' show exit;
@@ -265,6 +266,7 @@ abstract class BindingBase {
265266
assert(_debugInitializedType == null);
266267
assert(() {
267268
_debugInitializedType = runtimeType;
269+
_debugBindingZone = Zone.current;
268270
return true;
269271
}());
270272
}
@@ -319,7 +321,7 @@ abstract class BindingBase {
319321
),
320322
ErrorHint(
321323
'It is also possible that $T does not implement "initInstances()" to assign a value to "instance". See the '
322-
'documentation of the BaseBinding class for more details.',
324+
'documentation of the BindingBase class for more details.',
323325
),
324326
ErrorHint(
325327
'The binding that was initialized was of the type "$_debugInitializedType". '
@@ -399,6 +401,95 @@ abstract class BindingBase {
399401
return _debugInitializedType;
400402
}
401403

404+
Zone? _debugBindingZone;
405+
406+
/// Whether [debugCheckZone] should throw (true) or just report the error (false).
407+
///
408+
/// Setting this to true makes it easier to catch cases where the zones are
409+
/// misconfigured, by allowing debuggers to stop at the point of error.
410+
///
411+
/// Currently this defaults to false, to avoid suddenly breaking applications
412+
/// that are affected by this check but appear to be working today. Applications
413+
/// are encouraged to resolve any issues that cause the [debugCheckZone] message
414+
/// to appear, as even if they appear to be working today, they are likely to be
415+
/// hiding hard-to-find bugs, and are more brittle (likely to collect bugs in
416+
/// the future).
417+
///
418+
/// To silence the message displayed by [debugCheckZone], ensure that the same
419+
/// zone is used when calling `ensureInitialized()` as when calling the framework
420+
/// in any other context (e.g. via [runApp]).
421+
static bool debugZoneErrorsAreFatal = false;
422+
423+
/// Checks that the current [Zone] is the same as that which was used
424+
/// to initialize the binding.
425+
///
426+
/// If the current zone ([Zone.current]) is not the zone that was active when
427+
/// the binding was initialized, then this method generates a [FlutterError]
428+
/// exception with detailed information. The exception is either thrown
429+
/// directly, or reported via [FlutterError.reportError], depending on the
430+
/// value of [BindingBase.debugZoneErrorsAreFatal].
431+
///
432+
/// To silence the message displayed by [debugCheckZone], ensure that the same
433+
/// zone is used when calling `ensureInitialized()` as when calling the
434+
/// framework in any other context (e.g. via [runApp]). For example, consider
435+
/// keeping a reference to the zone used to initialize the binding, and using
436+
/// [Zone.run] to use it again when calling into the framework.
437+
///
438+
/// ## Usage
439+
///
440+
/// The binding is considered initialized once [BindingBase.initInstances] has
441+
/// run; if this is called before then, it will throw an [AssertionError].
442+
///
443+
/// The `entryPoint` parameter is the name of the API that is checking the
444+
/// zones are consistent, for example, `'runApp'`.
445+
///
446+
/// This function always returns true (if it does not throw). It is expected
447+
/// to be invoked via the binding instance, e.g.:
448+
///
449+
/// ```dart
450+
/// void startup() {
451+
/// WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
452+
/// assert(binding.debugCheckZone('startup'));
453+
/// // ...
454+
/// }
455+
/// ```
456+
///
457+
/// If the binding expects to be used with multiple zones, it should override
458+
/// this method to return true always without throwing. (For example, the
459+
/// bindings used with [flutter_test] do this as they make heavy use of zones
460+
/// to drive the framework with an artificial clock and to catch errors and
461+
/// report them as test failures.)
462+
bool debugCheckZone(String entryPoint) {
463+
assert(() {
464+
assert(_debugBindingZone != null, 'debugCheckZone can only be used after the binding is fully initialized.');
465+
if (Zone.current != _debugBindingZone) {
466+
final Error message = FlutterError(
467+
'Zone mismatch.\n'
468+
'The Flutter bindings were initialized in a different zone than is now being used. '
469+
'This will likely cause confusion and bugs as any zone-specific configuration will '
470+
'inconsistently use the configuration of the original binding initialization zone '
471+
'or this zone based on hard-to-predict factors such as which zone was active when '
472+
'a particular callback was set.\n'
473+
'It is important to use the same zone when calling `ensureInitialized` on the binding '
474+
'as when calling `$entryPoint` later.\n'
475+
'To make this ${ debugZoneErrorsAreFatal ? 'error non-fatal' : 'warning fatal' }, '
476+
'set BindingBase.debugZoneErrorsAreFatal to ${!debugZoneErrorsAreFatal} before the '
477+
'bindings are initialized (i.e. as the first statement in `void main() { }`).',
478+
);
479+
if (debugZoneErrorsAreFatal) {
480+
throw message;
481+
}
482+
FlutterError.reportError(FlutterErrorDetails(
483+
exception: message,
484+
stack: StackTrace.current,
485+
context: ErrorDescription('during $entryPoint'),
486+
));
487+
}
488+
return true;
489+
}());
490+
return true;
491+
}
492+
402493
/// Called when the binding is initialized, to register service
403494
/// extensions.
404495
///

packages/flutter/lib/src/widgets/binding.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
10311031
/// ensure the widget, element, and render trees are all built.
10321032
void runApp(Widget app) {
10331033
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
1034+
assert(binding.debugCheckZone('runApp'));
10341035
binding
10351036
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
10361037
..scheduleWarmUpFrame();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:flutter/foundation.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
class TestBinding extends BindingBase { }
11+
12+
void main() {
13+
test('BindingBase.debugCheckZone', () async {
14+
final BindingBase binding = TestBinding();
15+
binding.debugCheckZone('test1');
16+
BindingBase.debugZoneErrorsAreFatal = true;
17+
Zone.current.fork().run(() {
18+
try {
19+
binding.debugCheckZone('test2');
20+
fail('expected an exception');
21+
} catch (error) {
22+
expect(error, isA<FlutterError>());
23+
expect(error.toString(),
24+
'Zone mismatch.\n'
25+
'The Flutter bindings were initialized in a different zone than is now being used. '
26+
'This will likely cause confusion and bugs as any zone-specific configuration will '
27+
'inconsistently use the configuration of the original binding initialization zone '
28+
'or this zone based on hard-to-predict factors such as which zone was active when '
29+
'a particular callback was set.\n'
30+
'It is important to use the same zone when calling `ensureInitialized` on the '
31+
'binding as when calling `test2` later.\n'
32+
'To make this error non-fatal, set BindingBase.debugZoneErrorsAreFatal to false '
33+
'before the bindings are initialized (i.e. as the first statement in `void main() { }`).',
34+
);
35+
}
36+
});
37+
BindingBase.debugZoneErrorsAreFatal = false;
38+
Zone.current.fork().run(() {
39+
bool sawError = false;
40+
final FlutterExceptionHandler? lastHandler = FlutterError.onError;
41+
FlutterError.onError = (FlutterErrorDetails details) {
42+
final Object error = details.exception;
43+
expect(error, isA<FlutterError>());
44+
expect(error.toString(),
45+
'Zone mismatch.\n'
46+
'The Flutter bindings were initialized in a different zone than is now being used. '
47+
'This will likely cause confusion and bugs as any zone-specific configuration will '
48+
'inconsistently use the configuration of the original binding initialization zone '
49+
'or this zone based on hard-to-predict factors such as which zone was active when '
50+
'a particular callback was set.\n'
51+
'It is important to use the same zone when calling `ensureInitialized` on the '
52+
'binding as when calling `test3` later.\n'
53+
'To make this warning fatal, set BindingBase.debugZoneErrorsAreFatal to true '
54+
'before the bindings are initialized (i.e. as the first statement in `void main() { }`).',
55+
);
56+
sawError = true;
57+
};
58+
binding.debugCheckZone('test3');
59+
expect(sawError, isTrue);
60+
FlutterError.onError = lastHandler;
61+
});
62+
});
63+
}

packages/flutter/test/foundation/binding_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ class FooLibraryBinding extends BindingBase with FooBinding {
2727
}
2828
}
2929

30-
3130
void main() {
3231
test('BindingBase.debugBindingType', () async {
3332
expect(BindingBase.debugBindingType(), isNull);

packages/flutter/test/painting/binding_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class TestBindingBase implements BindingBase {
4747
@override
4848
void initInstances() {}
4949

50+
@override
51+
bool debugCheckZone(String entryPoint) { return true; }
52+
5053
@override
5154
void initServiceExtensions() {}
5255

0 commit comments

Comments
 (0)