Add function to set structured error early (#58118)

* Add function to set structured error early

* Remove unused imports

* Save test handler in setUp

* Add utils file for shared test service

* Rename structured error functions

* Use setUpAll to save error handler and call initStructuredError from other init

* Rename widget inspector test service

* Remove debugging print statement

* Move error handling setting back to initServiceExtensions

* Rename structured error handler in test

* Namespace environment variable

* Rename test
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 361ff1e..34469fb 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -744,6 +744,8 @@
   bool _trackRebuildDirtyWidgets = false;
   bool _trackRepaintWidgets = false;
 
+  FlutterExceptionHandler _structuredExceptionHandler;
+
   _RegisterServiceExtensionCallback _registerServiceExtensionCallback;
   /// Registers a service extension method with the given name (full
   /// name "ext.flutter.inspector.name").
@@ -941,6 +943,10 @@
     _errorsSinceReload = 0;
   }
 
+  bool isStructuredErrorsEnabled() {
+    return const bool.fromEnvironment('flutter.inspector.structuredErrors');
+  }
+
   /// Called to register service extensions.
   ///
   /// See also:
@@ -949,6 +955,10 @@
   ///  * [BindingBase.initServiceExtensions], which explains when service
   ///    extensions can be used.
   void initServiceExtensions(_RegisterServiceExtensionCallback registerServiceExtensionCallback) {
+    _structuredExceptionHandler = _reportError;
+    if (isStructuredErrorsEnabled()) {
+      FlutterError.onError = _structuredExceptionHandler;
+    }
     _registerServiceExtensionCallback = registerServiceExtensionCallback;
     assert(!_debugServiceExtensionsRegistered);
     assert(() {
@@ -958,14 +968,13 @@
 
     SchedulerBinding.instance.addPersistentFrameCallback(_onFrameStart);
 
-    final FlutterExceptionHandler structuredExceptionHandler = _reportError;
     final FlutterExceptionHandler defaultExceptionHandler = FlutterError.presentError;
 
     _registerBoolServiceExtension(
       name: 'structuredErrors',
-      getter: () async => FlutterError.presentError == structuredExceptionHandler,
+      getter: () async => FlutterError.presentError == _structuredExceptionHandler,
       setter: (bool value) {
-        FlutterError.presentError = value ? structuredExceptionHandler : defaultExceptionHandler;
+        FlutterError.presentError = value ? _structuredExceptionHandler : defaultExceptionHandler;
         return Future<void>.value();
       },
     );
diff --git a/packages/flutter/test/widgets/widget_inspector_init_with_structured_error_test.dart b/packages/flutter/test/widgets/widget_inspector_init_with_structured_error_test.dart
new file mode 100644
index 0000000..50fe302
--- /dev/null
+++ b/packages/flutter/test/widgets/widget_inspector_init_with_structured_error_test.dart
@@ -0,0 +1,63 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'widget_inspector_test_utils.dart';
+
+void main() {
+  StructuredErrorTestService.runTests();
+}
+
+class StructuredErrorTestService extends TestWidgetInspectorService {
+  @override
+  bool isStructuredErrorsEnabled() {
+    return true;
+  }
+
+  static void runTests() {
+    final StructuredErrorTestService service = StructuredErrorTestService();
+    WidgetInspectorService.instance = service;
+    FlutterExceptionHandler testHandler;
+    FlutterExceptionHandler inspectorServiceErrorHandler;
+
+    setUpAll(() {
+      inspectorServiceErrorHandler = FlutterError.onError;
+    });
+
+    setUp(() {
+      testHandler = FlutterError.onError;
+    });
+
+    testWidgets('ext.flutter.inspector.setStructuredErrors',
+        (WidgetTester tester) async {
+      // The test framework resets FlutterError.onError, so we set it back to
+      // what it was after WidgetInspectorService::initServiceExtensions ran.
+      FlutterError.onError = inspectorServiceErrorHandler;
+
+      List<Map<Object, Object>> flutterErrorEvents =
+          service.getEventsDispatched('Flutter.Error');
+      expect(flutterErrorEvents, hasLength(0));
+
+      // Create an error.
+      FlutterError.reportError(FlutterErrorDetailsForRendering(
+        library: 'rendering library',
+        context: ErrorDescription('during layout'),
+        exception: StackTrace.current,
+      ));
+
+      // Validate that we received an error.
+      flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
+      expect(flutterErrorEvents, hasLength(1));
+    });
+
+    tearDown(() {
+      FlutterError.onError = testHandler;
+    });
+  }
+}
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart
index e57ddcc..8cdd02a 100644
--- a/packages/flutter/test/widgets/widget_inspector_test.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -14,6 +14,8 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
+import 'widget_inspector_test_utils.dart';
+
 // Start of block of code where widget creation location line numbers and
 // columns will impact whether tests pass.
 
@@ -222,64 +224,10 @@
 }
 
 void main() {
-  TestWidgetInspectorService.runTests();
+  _TestWidgetInspectorService.runTests();
 }
 
-class TestWidgetInspectorService extends Object with WidgetInspectorService {
-  final Map<String, InspectorServiceExtensionCallback> extensions = <String, InspectorServiceExtensionCallback>{};
-
-  final Map<String, List<Map<Object, Object>>> eventsDispatched = <String, List<Map<Object, Object>>>{};
-
-  @override
-  void registerServiceExtension({
-    @required String name,
-    @required FutureOr<Map<String, Object>> callback(Map<String, String> parameters),
-  }) {
-    assert(!extensions.containsKey(name));
-    extensions[name] = callback;
-  }
-
-  @override
-  void postEvent(String eventKind, Map<Object, Object> eventData) {
-    getEventsDispatched(eventKind).add(eventData);
-  }
-
-  List<Map<Object, Object>> getEventsDispatched(String eventKind) {
-    return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object>>[]);
-  }
-
-  Iterable<Map<Object, Object>> getServiceExtensionStateChangedEvents(String extensionName) {
-    return getEventsDispatched('Flutter.ServiceExtensionStateChanged')
-      .where((Map<Object, Object> event) => event['extension'] == extensionName);
-  }
-
-  Future<Object> testExtension(String name, Map<String, String> arguments) async {
-    expect(extensions, contains(name));
-    // Encode and decode to JSON to match behavior using a real service
-    // extension where only JSON is allowed.
-    return json.decode(json.encode(await extensions[name](arguments)))['result'];
-  }
-
-  Future<String> testBoolExtension(String name, Map<String, String> arguments) async {
-    expect(extensions, contains(name));
-    // Encode and decode to JSON to match behavior using a real service
-    // extension where only JSON is allowed.
-    return json.decode(json.encode(await extensions[name](arguments)))['enabled'] as String;
-  }
-
-  int rebuildCount = 0;
-
-  @override
-  Future<void> forceRebuild() async {
-    rebuildCount++;
-    final WidgetsBinding binding = WidgetsBinding.instance;
-
-    if (binding.renderViewElement != null) {
-      binding.buildOwner.reassemble(binding.renderViewElement);
-    }
-  }
-
-
+class _TestWidgetInspectorService extends TestWidgetInspectorService {
   // These tests need access to protected members of WidgetInspectorService.
   static void runTests() {
     final TestWidgetInspectorService service = TestWidgetInspectorService();
@@ -1725,7 +1673,7 @@
       _CreationLocation location = knownLocations[id];
       expect(location.file, equals(file));
       // ClockText widget.
-      expect(location.line, equals(51));
+      expect(location.line, equals(53));
       expect(location.column, equals(9));
       expect(count, equals(1));
 
@@ -1734,7 +1682,7 @@
       location = knownLocations[id];
       expect(location.file, equals(file));
       // Text widget in _ClockTextState build method.
-      expect(location.line, equals(89));
+      expect(location.line, equals(91));
       expect(location.column, equals(12));
       expect(count, equals(1));
 
@@ -1759,7 +1707,7 @@
       location = knownLocations[id];
       expect(location.file, equals(file));
       // ClockText widget.
-      expect(location.line, equals(51));
+      expect(location.line, equals(53));
       expect(location.column, equals(9));
       expect(count, equals(3)); // 3 clock widget instances rebuilt.
 
@@ -1768,7 +1716,7 @@
       location = knownLocations[id];
       expect(location.file, equals(file));
       // Text widget in _ClockTextState build method.
-      expect(location.line, equals(89));
+      expect(location.line, equals(91));
       expect(location.column, equals(12));
       expect(count, equals(3)); // 3 clock widget instances rebuilt.
 
diff --git a/packages/flutter/test/widgets/widget_inspector_test_utils.dart b/packages/flutter/test/widgets/widget_inspector_test_utils.dart
new file mode 100644
index 0000000..f5d3184
--- /dev/null
+++ b/packages/flutter/test/widgets/widget_inspector_test_utils.dart
@@ -0,0 +1,68 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+typedef InspectorServiceExtensionCallback = FutureOr<Map<String, Object>> Function(Map<String, String> parameters);
+
+class TestWidgetInspectorService extends Object with WidgetInspectorService {
+  final Map<String, InspectorServiceExtensionCallback> extensions = <String, InspectorServiceExtensionCallback>{};
+
+  final Map<String, List<Map<Object, Object>>> eventsDispatched = <String, List<Map<Object, Object>>>{};
+
+  @override
+  void registerServiceExtension({
+    @required String name,
+    @required FutureOr<Map<String, Object>> callback(Map<String, String> parameters),
+  }) {
+    assert(!extensions.containsKey(name));
+    extensions[name] = callback;
+  }
+
+  @override
+  void postEvent(String eventKind, Map<Object, Object> eventData) {
+    getEventsDispatched(eventKind).add(eventData);
+  }
+
+  List<Map<Object, Object>> getEventsDispatched(String eventKind) {
+    return eventsDispatched.putIfAbsent(eventKind, () => <Map<Object, Object>>[]);
+  }
+
+  Iterable<Map<Object, Object>> getServiceExtensionStateChangedEvents(String extensionName) {
+    return getEventsDispatched('Flutter.ServiceExtensionStateChanged')
+      .where((Map<Object, Object> event) => event['extension'] == extensionName);
+  }
+
+  Future<Object> testExtension(String name, Map<String, String> arguments) async {
+    expect(extensions, contains(name));
+    // Encode and decode to JSON to match behavior using a real service
+    // extension where only JSON is allowed.
+    return json.decode(json.encode(await extensions[name](arguments)))['result'];
+  }
+
+  Future<String> testBoolExtension(String name, Map<String, String> arguments) async {
+    expect(extensions, contains(name));
+    // Encode and decode to JSON to match behavior using a real service
+    // extension where only JSON is allowed.
+    return json.decode(json.encode(await extensions[name](arguments)))['enabled'] as String;
+  }
+
+  int rebuildCount = 0;
+
+  @override
+  Future<void> forceRebuild() async {
+    rebuildCount++;
+    final WidgetsBinding binding = WidgetsBinding.instance;
+
+    if (binding.renderViewElement != null) {
+      binding.buildOwner.reassemble(binding.renderViewElement);
+    }
+  }
+}