From eb58fe16b60f38ab403b75edd3805e884f4eddb8 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Fri, 28 Jun 2024 07:36:03 +0100 Subject: [PATCH] [flutter_tools] Include more details in structured errors sent to a DAP client (#150698) The debug adapter converts Flutter's structured errors into a text format to be sent to the debug client and shown in the console. When an error is not the first error since the last reload, it is shown as just a summary (since it may be caused by a prior error). In this mode, the filter was causing some important information (the erroring widget) to be omitted. This tweaks the logic to include child nodes of a `DiagnosticBlock` in this mode. Fixes https://github.com/Dart-Code/Dart-Code/issues/4743 ## Before: ![image](https://github.com/flutter/flutter/assets/1078012/46ccd2ef-b165-46b4-a8ab-4473f82a904c) ## After: ![image](https://github.com/flutter/flutter/assets/1078012/232f866e-cf6f-4016-9d1d-49323204da04) --- .../src/debug_adapters/error_formatter.dart | 11 +++- .../dap/flutter_adapter_test.dart | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart index e55c235ea45ce..99987a2980055 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/error_formatter.dart @@ -118,7 +118,11 @@ class FlutterErrorFormatter { final bool allChildrenAreLeaf = node.children.isNotEmpty && !node.children.any((_ErrorNode child) => child.children.isNotEmpty); if (node.level == _DiagnosticsNodeLevel.summary || allChildrenAreLeaf) { - _writeNode(node, recursive: false); + // DiagnosticsBlock is a container, so recurse into its children if + // there's only a single level. The container may be + // "The relevant error-causing widget was" and the child may be + // the specific widget details. + _writeNode(node, recursive: node.type == _DiagnosticsNodeType.DiagnosticsBlock && allChildrenAreLeaf); } } } @@ -148,6 +152,10 @@ enum _DiagnosticsNodeStyle { flat, } +enum _DiagnosticsNodeType { + DiagnosticsBlock, +} + class _ErrorData extends _ErrorNode { _ErrorData(super.data); @@ -167,6 +175,7 @@ class _ErrorNode { List<_ErrorNode> get properties => asList('properties', _ErrorNode.new); bool get showName => data['showName'] != false; _DiagnosticsNodeStyle? get style => asEnum('style', _DiagnosticsNodeStyle.values); + _DiagnosticsNodeType? get type => asEnum('type', _DiagnosticsNodeType.values); String? asString(String field) { final Object? value = data[field]; diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index fbb225f4ea0f3..a5e0d09c110c6 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -3,12 +3,14 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:dds/dap.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/debug_adapters/error_formatter.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart'; import 'package:flutter_tools/src/globals.dart' as globals show fs, platform; @@ -797,6 +799,55 @@ void main() { expect(adapter.processArgs, contains('tool_args')); }); }); + + group('error formatter', () { + /// Helpers to build a string representation of the DAP OutputEvents for + /// the structured error [errorData]. + String getFormattedError(Map errorData) { + // Format the error and write into a buffer in a text format convenient + // for test expectations. + final StringBuffer buffer = StringBuffer(); + FlutterErrorFormatter() + ..formatError(errorData) + ..sendOutput((String category, String message, {bool? parseStackFrames, int? variablesReference}) { + buffer.writeln('${category.padRight(6)} ${jsonEncode(message)}'); + }); + return buffer.toString(); + } + + test('includes children of DiagnosticsBlock when writing a summary', () { + // Format a simulated error that nests the error-causing widget in a + // diagnostic block and will be displayed in summary mode (because it + // is not the first error since the last reload). + // https://github.com/Dart-Code/Dart-Code/issues/4743 + final String error = getFormattedError({ + 'errorsSinceReload': 1, // Force summary mode + 'description': 'Exception caught...', + 'properties': >[ + { + 'description': 'The following assertion was thrown...', + }, + { + 'description': '', + 'type': 'DiagnosticsBlock', + 'name': 'The relevant error-causing widget was', + 'children': >[ + { + 'description': 'MyWidget:file:///path/to/widget.dart:1:2', + } + ] + } + ], + }); + + expect(error, r''' +stdout "\n" +stderr "════════ Exception caught... ═══════════════════════════════════════════════════\n" +stdout "The relevant error-causing widget was:\n MyWidget:file:///path/to/widget.dart:1:2\n" +stderr "════════════════════════════════════════════════════════════════════════════════\n" +'''); + }); + }); }); }