diff --git a/packages/ui_primitives/lib/src/foundation/_platform_io.dart b/packages/ui_primitives/lib/src/foundation/_platform_io.dart index 985832770..506320c43 100644 --- a/packages/ui_primitives/lib/src/foundation/_platform_io.dart +++ b/packages/ui_primitives/lib/src/foundation/_platform_io.dart @@ -37,7 +37,7 @@ platform.TargetPlatform get defaultTargetPlatform { result = platform.debugDefaultTargetPlatformOverride; } if (result == null) { - throw UiError( + throw FrameworkError( 'Unknown platform.\n' '${Platform.operatingSystem} was not recognized as a target platform. ' 'Consider updating the list of TargetPlatforms to include this platform.', diff --git a/packages/ui_primitives/lib/src/foundation/assertions.dart b/packages/ui_primitives/lib/src/foundation/assertions.dart index b59c7e7b0..a56031078 100644 --- a/packages/ui_primitives/lib/src/foundation/assertions.dart +++ b/packages/ui_primitives/lib/src/foundation/assertions.dart @@ -31,14 +31,14 @@ export 'stack_frame.dart' show StackFrame; // class Trace implements StackTrace { late StackTrace vmTrace; } // class Chain implements StackTrace { Trace toTrace() => Trace(); } -/// Signature for [UiError.onError] handler. -typedef FlutterExceptionHandler = void Function(UiErrorDetails details); +/// Signature for [FrameworkError.onError] handler. +typedef FlutterExceptionHandler = void Function(FrameworkErrorDetails details); /// Signature for [DiagnosticPropertiesBuilder] transformer. typedef DiagnosticPropertiesTransformer = Iterable Function(Iterable properties); -/// Signature for [UiErrorDetails.informationCollector] callback +/// Signature for [FrameworkErrorDetails.informationCollector] callback /// and other callbacks that collect information describing an error. typedef InformationCollector = Iterable Function(); @@ -47,7 +47,7 @@ typedef InformationCollector = Iterable Function(); /// /// See also: /// -/// * [UiError.demangleStackTrace], which shows an example +/// * [FrameworkError.demangleStackTrace], which shows an example /// implementation. typedef StackTraceDemangler = StackTrace Function(StackTrace details); @@ -108,7 +108,7 @@ class PartialStackFrame { } /// A class that filters stack frames for additional filtering on -/// [UiError.defaultStackFilter]. +/// [FrameworkError.defaultStackFilter]. abstract class StackFilter { /// This constructor enables subclasses to provide const constructors so that /// they can be used in const expressions. @@ -126,8 +126,8 @@ abstract class StackFilter { /// /// See also: /// -/// * [UiError.addDefaultStackFilter], a method to register additional -/// stack filters for [UiError.defaultStackFilter]. +/// * [FrameworkError.addDefaultStackFilter], a method to register additional +/// stack filters for [FrameworkError.defaultStackFilter]. /// * [StackFrame], a class that can help with parsing stack frames. /// * [PartialStackFrame], a class that helps match partial method information /// to a stack frame. @@ -147,7 +147,7 @@ class RepetitiveStackFrameFilter extends StackFilter { /// The string to replace the frames with. /// /// If the same replacement string is used multiple times in a row, the - /// [UiError.defaultStackFilter] will insert a repeat count after this + /// [FrameworkError.defaultStackFilter] will insert a repeat count after this /// line rather than repeating it. final String replacement; @@ -264,7 +264,7 @@ abstract class _ErrorDiagnostic extends DiagnosticsProperty> { /// * [ErrorHint], which provides specific, non-obvious advice that may be /// applicable. /// * [ErrorSpacer], which renders as a blank line. -/// * [UiError], which is the most common place to use an +/// * [FrameworkError], which is the most common place to use an /// [ErrorDescription]. class ErrorDescription extends _ErrorDiagnostic { /// A lint enforces that this constructor can only be called with a string @@ -291,7 +291,7 @@ class ErrorDescription extends _ErrorDiagnostic { /// so that they can be recognized as related. For example, they shouldn't /// include hash codes. /// -/// A [UiError] must start with an [ErrorSummary] and may not contain +/// A [FrameworkError] must start with an [ErrorSummary] and may not contain /// multiple summaries. /// /// In debug builds, values interpolated into the `message` are @@ -305,7 +305,7 @@ class ErrorDescription extends _ErrorDiagnostic { /// information, etc. /// * [ErrorHint], which provides specific, non-obvious advice that may be /// applicable. -/// * [UiError], which is the most common place to use an [ErrorSummary]. +/// * [FrameworkError], which is the most common place to use an [ErrorSummary]. class ErrorSummary extends _ErrorDiagnostic { /// A lint enforces that this constructor can only be called with a string /// literal to match the limitations of the Dart Kernel transformer that @@ -342,7 +342,7 @@ class ErrorSummary extends _ErrorDiagnostic { /// cause, any information that may help track down the problem, background /// information, etc. /// * [ErrorSpacer], which renders as a blank line. -/// * [UiError], which is the most common place to use an [ErrorHint]. +/// * [FrameworkError], which is the most common place to use an [ErrorHint]. class ErrorHint extends _ErrorDiagnostic { /// A lint enforces that this constructor can only be called with a string /// literal to match the limitations of the Dart Kernel transformer that @@ -366,15 +366,15 @@ class ErrorHint extends _ErrorDiagnostic { /// tune the spacing between other [DiagnosticsNode] objects. class ErrorSpacer extends DiagnosticsProperty { /// Creates an empty space to insert into a list of [DiagnosticsNode] objects - /// typically within a [UiError] object. + /// typically within a [FrameworkError] object. ErrorSpacer() : super('', null, description: '', showName: false); } /// Class for information provided to [FlutterExceptionHandler] callbacks. /// /// {@tool snippet} -/// This is an example of using [UiErrorDetails] when calling -/// [UiError.reportError]. +/// This is an example of using [FrameworkErrorDetails] when calling +/// [FrameworkError.reportError]. /// /// ```dart /// void main() { @@ -394,15 +394,15 @@ class ErrorSpacer extends DiagnosticsProperty { /// /// See also: /// -/// * [UiError.onError], which is called whenever the Flutter framework +/// * [FrameworkError.onError], which is called whenever the Flutter framework /// catches an error. -class UiErrorDetails with Diagnosticable { - /// Creates a [UiErrorDetails] object with the given arguments setting +class FrameworkErrorDetails with Diagnosticable { + /// Creates a [FrameworkErrorDetails] object with the given arguments setting /// the object's properties. /// /// The framework calls this constructor when catching an exception that will - /// subsequently be reported using [UiError.onError]. - const UiErrorDetails({ + /// subsequently be reported using [FrameworkError.onError]. + const FrameworkErrorDetails({ required this.exception, this.stack, this.library = 'Flutter framework', @@ -414,7 +414,7 @@ class UiErrorDetails with Diagnosticable { /// Creates a copy of the error details but with the given fields replaced /// with new values. - UiErrorDetails copyWith({ + FrameworkErrorDetails copyWith({ DiagnosticsNode? context, Object? exception, InformationCollector? informationCollector, @@ -423,7 +423,7 @@ class UiErrorDetails with Diagnosticable { StackTrace? stack, IterableFilter? stackFilter, }) { - return UiErrorDetails( + return FrameworkErrorDetails( context: context ?? this.context, exception: exception ?? this.exception, informationCollector: informationCollector ?? this.informationCollector, @@ -439,7 +439,7 @@ class UiErrorDetails with Diagnosticable { /// into a more descriptive form. /// /// There are layers that attach certain [DiagnosticsNode] into - /// [UiErrorDetails] that require knowledge from other layers to parse. + /// [FrameworkErrorDetails] that require knowledge from other layers to parse. /// To correctly interpret those [DiagnosticsNode], register transformers in /// the layers that possess the knowledge. /// @@ -452,7 +452,7 @@ class UiErrorDetails with Diagnosticable { /// The exception. /// /// Often this will be an [AssertionError], maybe specifically a - /// [UiError]. + /// [FrameworkError]. /// However, this could be any value at all. final Object exception; @@ -464,7 +464,7 @@ class UiErrorDetails with Diagnosticable { /// If this field is not null, then the [stackFilter] callback, if any, will /// be called with the result of calling [toString] on this object and /// splitting that result on line breaks. If there's no [stackFilter] - /// callback, then [UiError.defaultStackFilter] is used instead. That + /// callback, then [FrameworkError.defaultStackFilter] is used instead. That /// function expects the stack to be in the format used by /// [StackTrace.toString]. final StackTrace? stack; @@ -486,7 +486,7 @@ class UiErrorDetails with Diagnosticable { /// /// {@tool snippet} /// This is an example of using and [ErrorDescription] as the - /// [UiErrorDetails.context] when calling [UiError.reportError]. + /// [FrameworkErrorDetails.context] when calling [FrameworkError.reportError]. /// /// ```dart /// void maybeDoSomething() { @@ -514,8 +514,8 @@ class UiErrorDetails with Diagnosticable { /// problem that was detected. /// * [ErrorHint], which provides specific, non-obvious advice that may be /// applicable. - /// * [UiError], which is the most common place to use - /// [UiErrorDetails]. + /// * [FrameworkError], which is the most common place to use + /// [FrameworkErrorDetails]. final DiagnosticsNode? context; /// A callback which filters the [stack] trace. @@ -525,10 +525,10 @@ class UiErrorDetails with Diagnosticable { /// to /// output for the stack. /// - /// If this is not provided, then [UiError.dumpErrorToConsole] will use - /// [UiError.defaultStackFilter] instead. + /// If this is not provided, then [FrameworkError.dumpErrorToConsole] will use + /// [FrameworkError.defaultStackFilter] instead. /// - /// If the [UiError.defaultStackFilter] behavior is desired, then the + /// If the [FrameworkError.defaultStackFilter] behavior is desired, then the /// callback should manually call that function. That function expects the /// incoming list to be in the [StackTrace.toString()] format. The output of /// that function, however, does not always follow this format. @@ -654,11 +654,11 @@ class UiErrorDetails with Diagnosticable { Diagnosticable? _exceptionToDiagnosticable() { final Object exception = this.exception; - if (exception is UiError) { + if (exception is FrameworkError) { return exception; } - if (exception is AssertionError && exception.message is UiError) { - return exception.message! as UiError; + if (exception is AssertionError && exception.message is FrameworkError) { + return exception.message! as FrameworkError; } return null; } @@ -730,7 +730,7 @@ class UiErrorDetails with Diagnosticable { // If so: Error is in Framework. We either need an assertion higher up // in the stack, or we've violated our own assertions. final List stackFrames = - StackFrame.fromStackTrace(UiError.demangleStackTrace(stack!)) + StackFrame.fromStackTrace(FrameworkError.demangleStackTrace(stack!)) .skipWhile((StackFrame frame) => frame.packageScheme == 'dart') .toList(); final bool ourFault = @@ -791,14 +791,9 @@ class UiErrorDetails with Diagnosticable { } } -/// Error class used to report Flutter-specific assertion failures and +/// Error class used to report Framework-specific assertion failures and /// contract violations. -/// -/// See also: -/// -/// * , more information about error -/// handling in Flutter. -class UiError extends Error +class FrameworkError extends Error with DiagnosticableTreeMixin implements AssertionError { /// Create an error message from a string. @@ -809,7 +804,7 @@ class UiError extends Error /// substantial additional information, ideally sufficient to develop a /// correct solution to the problem. /// - /// In some cases, when a [UiError] is reported to the user, only the + /// In some cases, when a [FrameworkError] is reported to the user, only the /// first /// line is included. For example, Flutter will typically only fully report /// the first exception at runtime, displaying only the first line of @@ -823,9 +818,9 @@ class UiError extends Error /// lines are wrapped in implied [ErrorDescription]s. Consider using the /// [FlutterError.fromParts] constructor to provide more detail, e.g. /// using [ErrorHint]s or other [DiagnosticsNode]s. - factory UiError(String message) { + factory FrameworkError(String message) { final List lines = message.split('\n'); - return UiError.fromParts([ + return FrameworkError.fromParts([ ErrorSummary(lines.first), ...lines.skip(1).map(ErrorDescription.new), ]); @@ -882,22 +877,22 @@ class UiError extends Error /// } /// ``` /// {@end-tool} - UiError.fromParts(this.diagnostics) + FrameworkError.fromParts(this.diagnostics) : assert( diagnostics.isNotEmpty, - UiError.fromParts([ + FrameworkError.fromParts([ ErrorSummary('Empty FlutterError'), ]), ) { assert( diagnostics.first.level == DiagnosticLevel.summary, - UiError.fromParts([ + FrameworkError.fromParts([ ErrorSummary('FlutterError is missing a summary.'), ErrorDescription( 'All FlutterError objects should start with a short (one line) ' 'summary description of the problem that was detected.', ), - DiagnosticsProperty( + DiagnosticsProperty( 'Malformed', this, expandableValue: true, @@ -924,7 +919,7 @@ class UiError extends Error '(one line) summary description of the problem that was ' 'detected.', ), - DiagnosticsProperty( + DiagnosticsProperty( 'Malformed', this, expandableValue: true, @@ -954,7 +949,7 @@ class UiError extends Error ' https://github.com/flutter/flutter/issues/new?template=02_bug.yml', ), ); - throw UiError.fromParts(message); + throw FrameworkError.fromParts(message); } return true; }()); @@ -1073,7 +1068,7 @@ class UiError extends Error /// /// The default behavior for the [onError] handler is to call this function. static void dumpErrorToConsole( - UiErrorDetails details, { + FrameworkErrorDetails details, { bool forceReport = false, }) { var isInDebugMode = false; @@ -1132,7 +1127,7 @@ class UiError extends Error /// frames that correspond to Dart internals. /// /// This is the default filter used by [dumpErrorToConsole] if the - /// [UiErrorDetails] object has no [UiErrorDetails.stackFilter] + /// [FrameworkErrorDetails] object has no [FrameworkErrorDetails.stackFilter] /// callback. /// /// This function expects its input to be in the format used by @@ -1269,13 +1264,13 @@ class UiError extends Error /// } /// ``` /// {@end-tool} - static void reportError(UiErrorDetails details) { + static void reportError(FrameworkErrorDetails details) { onError?.call(details); } } /// Dump the stack to the console using [debugPrint] and -/// [UiError.defaultStackFilter]. +/// [FrameworkError.defaultStackFilter]. /// /// If the `stackTrace` parameter is null, the [StackTrace.current] is used to /// obtain the stack. @@ -1292,7 +1287,7 @@ void debugPrintStack({StackTrace? stackTrace, String? label, int? maxFrames}) { if (stackTrace == null) { stackTrace = StackTrace.current; } else { - stackTrace = UiError.demangleStackTrace(stackTrace); + stackTrace = FrameworkError.demangleStackTrace(stackTrace); } Iterable lines = stackTrace.toString().trimRight().split('\n'); if (kIsWeb && lines.isNotEmpty) { @@ -1308,18 +1303,20 @@ void debugPrintStack({StackTrace? stackTrace, String? label, int? maxFrames}) { if (maxFrames != null) { lines = lines.take(maxFrames); } - ErrorToConsoleDumper.dump(UiError.defaultStackFilter(lines).join('\n')); + ErrorToConsoleDumper.dump( + FrameworkError.defaultStackFilter(lines).join('\n'), + ); } /// Diagnostic with a [StackTrace] [value] suitable for displaying stack traces -/// as part of a [UiError] object. +/// as part of a [FrameworkError] object. class DiagnosticsStackTrace extends DiagnosticsBlock { /// Creates a diagnostic for a stack trace. /// /// [name] describes a name the stack trace is given, e.g. /// `When the exception was thrown, this was the stack`. /// [stackFilter] provides an optional filter to use to filter which frames - /// are included. If no filter is specified, [UiError.defaultStackFilter] + /// are included. If no filter is specified, [FrameworkError.defaultStackFilter] /// is used. /// [showSeparator] indicates whether to include a ':' after the [name]. DiagnosticsStackTrace( @@ -1354,9 +1351,9 @@ class DiagnosticsStackTrace extends DiagnosticsBlock { return []; } final IterableFilter filter = - stackFilter ?? UiError.defaultStackFilter; + stackFilter ?? FrameworkError.defaultStackFilter; final Iterable frames = filter( - '${UiError.demangleStackTrace(stack)}'.trimRight().split('\n'), + '${FrameworkError.demangleStackTrace(stack)}'.trimRight().split('\n'), ); return frames.map(_createStackFrame).toList(); } @@ -1369,7 +1366,8 @@ class DiagnosticsStackTrace extends DiagnosticsBlock { bool get allowTruncate => false; } -class _FlutterErrorDetailsNode extends DiagnosticableNode { +class _FlutterErrorDetailsNode + extends DiagnosticableNode { _FlutterErrorDetailsNode({ super.name, required super.value, @@ -1384,7 +1382,7 @@ class _FlutterErrorDetailsNode extends DiagnosticableNode { } Iterable properties = builder.properties; for (final DiagnosticPropertiesTransformer transformer - in UiErrorDetails.propertiesTransformers) { + in FrameworkErrorDetails.propertiesTransformers) { properties = transformer(properties); } return DiagnosticPropertiesBuilder.fromProperties(properties.toList()); diff --git a/packages/ui_primitives/lib/src/foundation/debug.dart b/packages/ui_primitives/lib/src/foundation/debug.dart index 4e1a6ec81..21c39420d 100644 --- a/packages/ui_primitives/lib/src/foundation/debug.dart +++ b/packages/ui_primitives/lib/src/foundation/debug.dart @@ -38,7 +38,7 @@ bool debugAssertAllFoundationVarsUnset( debugDefaultTargetPlatformOverride != null || debugDoublePrecision != null || debugBrightnessOverride != null) { - throw UiError(reason); + throw FrameworkError(reason); } return true; }()); diff --git a/packages/ui_primitives/lib/src/foundation/diagnostics.dart b/packages/ui_primitives/lib/src/foundation/diagnostics.dart index ff2063799..702371e98 100644 --- a/packages/ui_primitives/lib/src/foundation/diagnostics.dart +++ b/packages/ui_primitives/lib/src/foundation/diagnostics.dart @@ -174,7 +174,7 @@ enum DiagnosticsTreeStyle { /// /// See also: /// - /// * [UiError], which uses this style for the root node in a tree + /// * [FrameworkError], which uses this style for the root node in a tree /// describing an error. error, @@ -1681,7 +1681,7 @@ abstract class DiagnosticsNode { // We don't throw in release builds, to avoid hurting users. We also don't // do anything useful. if (kProfileMode) { - throw UiError( + throw FrameworkError( // Parts of this string are searched for verbatim by a test in // dev/bots/test.dart. '$DiagnosticsNode.toTimelineArguments used in non-debug build.\n' diff --git a/packages/ui_primitives/lib/src/foundation/platform.dart b/packages/ui_primitives/lib/src/foundation/platform.dart index a4c3cacf3..b91e0dd74 100644 --- a/packages/ui_primitives/lib/src/foundation/platform.dart +++ b/packages/ui_primitives/lib/src/foundation/platform.dart @@ -108,7 +108,7 @@ TargetPlatform? get debugDefaultTargetPlatformOverride => set debugDefaultTargetPlatformOverride(TargetPlatform? value) { if (!kDebugMode) { - throw UiError( + throw FrameworkError( 'Cannot modify debugDefaultTargetPlatformOverride in non-debug builds.', ); } diff --git a/packages/ui_primitives/lib/src/foundation/value_notifier.dart b/packages/ui_primitives/lib/src/foundation/value_notifier.dart index 0419f1db5..22624eb4b 100644 --- a/packages/ui_primitives/lib/src/foundation/value_notifier.dart +++ b/packages/ui_primitives/lib/src/foundation/value_notifier.dart @@ -94,7 +94,7 @@ class _ChangeNotifier implements Listenable { static bool debugAssertNotDisposed(_ChangeNotifier notifier) { assert(() { if (notifier._debugDisposed) { - throw UiError( + throw FrameworkError( 'A ${notifier.runtimeType} was used after being disposed.\n' 'Once you have called dispose() on a ${notifier.runtimeType}, it ' 'can no longer be used.', @@ -281,7 +281,7 @@ class _ChangeNotifier implements Listenable { /// not be visited after they are removed. /// /// Exceptions thrown by listeners will be caught and reported using - /// [UiError.reportError]. + /// [FrameworkError.reportError]. /// /// This method must not be called after [dispose] has been called. /// @@ -316,8 +316,8 @@ class _ChangeNotifier implements Listenable { try { _listeners[i]?.call(); } catch (exception, stack) { - UiError.reportError( - UiErrorDetails( + FrameworkError.reportError( + FrameworkErrorDetails( exception: exception, stack: stack, library: 'foundation library', @@ -414,7 +414,7 @@ class ValueNotifier implements ValueListenable { static bool debugAssertNotDisposed(ValueNotifier notifier) { assert(() { if (notifier._debugDisposed) { - throw UiError( + throw FrameworkError( 'A ${notifier.runtimeType} was used after being disposed.\n' 'Once you have called dispose() on a ${notifier.runtimeType}, it ' 'can no longer be used.', diff --git a/packages/ui_primitives/lib/ui_primitives.dart b/packages/ui_primitives/lib/ui_primitives.dart index 22efc94af..5fe02dd33 100644 --- a/packages/ui_primitives/lib/ui_primitives.dart +++ b/packages/ui_primitives/lib/ui_primitives.dart @@ -13,8 +13,8 @@ export 'src/foundation/assertions.dart' RepetitiveStackFrameFilter, StackFilter, StackFrame, - UiError, - UiErrorDetails, + FrameworkError, + FrameworkErrorDetails, debugPrintStack; export 'src/foundation/diagnostics.dart' diff --git a/packages/ui_primitives/pubspec.yaml b/packages/ui_primitives/pubspec.yaml index 79ac07e33..d52ed42c5 100644 --- a/packages/ui_primitives/pubspec.yaml +++ b/packages/ui_primitives/pubspec.yaml @@ -5,7 +5,7 @@ name: ui_primitives description: Highly experimental package. repository: https://github.com/flutter/genui/tree/main/packages/ui_primitives -version: 0.0.1-dev-005 +version: 0.0.1-dev-006 resolution: workspace diff --git a/packages/ui_primitives/test/error_reporting_test.dart b/packages/ui_primitives/test/error_reporting_test.dart new file mode 100644 index 000000000..89a743c88 --- /dev/null +++ b/packages/ui_primitives/test/error_reporting_test.dart @@ -0,0 +1,318 @@ +// 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:test/test.dart'; +import 'package:ui_primitives/ui_primitives.dart'; + +Object getAssertionErrorWithMessage() { + try { + assert(false, 'Message goes here.'); + } catch (e) { + return e; + } + throw Error(); +} + +Object getAssertionErrorWithoutMessage() { + try { + assert(false); + } catch (e) { + return e; + } + throw Error(); +} + +Object getAssertionErrorWithLongMessage() { + try { + assert(false, 'word ' * 100); + } catch (e) { + return e; + } + // ignore: only_throw_errors + throw 'assert failed'; +} + +Future getSampleStack() async { + return Future.sync(() => StackTrace.current); +} + +String _setPath(String source) { + return source.replaceAll( + r'$thisTestPath', + 'ui_primitives/test/error_reporting_test.dart', + ); +} + +Future main() async { + final console = []; + + final StackTrace sampleStack = await getSampleStack(); + + setUp(() async { + expect(debugPrint, equals(debugPrintThrottled)); + debugPrint = (String? message, {int? wrapWidth}) { + console.add(message); + }; + }); + + tearDown(() async { + expect(console, isEmpty); + debugPrint = debugPrintThrottled; + }); + + test('Error reporting - assert with message', () async { + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails( + exception: getAssertionErrorWithMessage(), + stack: sampleStack, + library: 'error handling test', + context: ErrorDescription('testing the error handling logic'), + informationCollector: () sync* { + yield ErrorDescription('line 1 of extra information'); + yield ErrorHint('line 2 of extra information\n'); + }, + ), + ); + expect( + console.join('\n'), + matches( + _setPath( + r'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n' + r'The following assertion was thrown testing the error handling logic:\n' + r'Message goes here\.\n' + r"'[^']+$thisTestPath':\n" + r"Failed assertion: line [0-9]+ pos [0-9]+: 'false'\n" + r'\n' + r'When the exception was thrown, this was the stack:\n' + r'#0 getSampleStack\. \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'#2 getSampleStack \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'#3 main \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'(.+\n)+', // TODO(ianh): when fixing #4021, also filter out frames from the test infrastructure below the first call to our main() + ), + ), + ); + console.clear(); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: getAssertionErrorWithMessage()), + ); + expect( + console.join('\n'), + 'Another exception was thrown: Message goes here.', + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + test('Error reporting - assert with long message', () async { + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: getAssertionErrorWithLongMessage()), + ); + expect( + console.join('\n'), + matches( + _setPath( + r'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n' + r'The following assertion was thrown:\n' + r'word word word word word word word word word word word word word word word word word word word word\n' + r'word word word word word word word word word word word word word word word word word word word word\n' + r'word word word word word word word word word word word word word word word word word word word word\n' + r'word word word word word word word word word word word word word word word word word word word word\n' + r'word word word word word word word word word word word word word word word word word word word word\n' + r"'[^']+$thisTestPath':\n" + r"Failed assertion: line [0-9]+ pos [0-9]+: 'false'\n" + r'════════════════════════════════════════════════════════════════════════════════════════════════════$', + ), + ), + ); + console.clear(); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: getAssertionErrorWithLongMessage()), + ); + expect( + console.join('\n'), + 'Another exception was thrown: ' + 'word word word word word word word word word word word word word word word word word word word word ' + 'word word word word word word word word word word word word word word word word word word word word ' + 'word word word word word word word word word word word word word word word word word word word word ' + 'word word word word word word word word word word word word word word word word word word word word ' + 'word word word word word word word word word word word word word word word word word word word word', + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + test('Error reporting - assert with no message', () async { + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails( + exception: getAssertionErrorWithoutMessage(), + stack: sampleStack, + library: 'error handling test', + context: ErrorDescription('testing the error handling logic'), + informationCollector: () sync* { + yield ErrorDescription('line 1 of extra information'); + yield ErrorDescription( + 'line 2 of extra information\n', + ); // the trailing newlines here are intentional + }, + ), + ); + expect( + console.join('\n'), + matches( + _setPath( + r'^══╡ EXCEPTION CAUGHT BY ERROR HANDLING TEST ╞═══════════════════════════════════════════════════════\n' + r'The following assertion was thrown testing the error handling logic:\n' + r"'[^']+$thisTestPath':[\n ]" + r"Failed[\n ]assertion:[\n ]line[\n ][0-9]+[\n ]pos[\n ][0-9]+:[\n ]'false':[\n ]is[\n ]not[\n ]true\.\n" + r'\n' + r'When the exception was thrown, this was the stack:\n' + r'#0 getSampleStack\. \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'#2 getSampleStack \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'#3 main \([^)]+$thisTestPath:[0-9]+:[0-9]+\)\n' + r'(.+\n)+', // TODO(ianh): when fixing #4021, also filter out frames from the test infrastructure below the first call to our main() + ), + ), + ); + console.clear(); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: getAssertionErrorWithoutMessage()), + ); + expect( + console.join('\n'), + matches( + _setPath( + r"Another exception was thrown: '[^']+$thisTestPath': Failed assertion: line [0-9]+ pos [0-9]+: 'false': is not true\.", + ), + ), + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + test('Error reporting - NoSuchMethodError', () async { + expect(console, isEmpty); + final Object exception = NoSuchMethodError.withInvocation( + 5, + Invocation.method(#foo, [2, 4]), + ); + + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: exception), + ); + expect( + console.join('\n'), + matches( + r'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n' + r'The following NoSuchMethodError was thrown:\n' + r'int has no foo method accepting arguments \(_, _\)\n' + r'════════════════════════════════════════════════════════════════════════════════════════════════════$', + ), + ); + console.clear(); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails(exception: exception), + ); + expect( + console.join('\n'), + 'Another exception was thrown: NoSuchMethodError: int has no foo method accepting arguments (_, _)', + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + test('Error reporting - NoSuchMethodError', () async { + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + const FrameworkErrorDetails(exception: 'hello'), + ); + expect( + console.join('\n'), + matches( + r'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n' + r'The following message was thrown:\n' + r'hello\n' + r'════════════════════════════════════════════════════════════════════════════════════════════════════$', + ), + ); + console.clear(); + FrameworkError.dumpErrorToConsole( + const FrameworkErrorDetails(exception: 'hello again'), + ); + expect(console.join('\n'), 'Another exception was thrown: hello again'); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/62223 + test('Error reporting - empty stack', () async { + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails( + exception: 'exception - empty stack', + stack: StackTrace.fromString(''), + ), + ); + expect( + console.join('\n'), + matches( + r'^══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════\n' + r'The following message was thrown:\n' + r'exception - empty stack\n' + r'\n' + r'When the exception was thrown, this was the stack\n' + r'════════════════════════════════════════════════════════════════════════════════════════════════════$', + ), + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); + + test('Stack traces are not truncated', () async { + const stackString = ''' +#0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:42:39) +#1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:38:5) +#2 new Text (package:flutter/src/widgets/text.dart:287:10) +#3 _MyHomePageState.build (package:hello_flutter/main.dart:72:16) +#4 StatefulElement.build (package:flutter/src/widgets/framework.dart:4414:27) +#5 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4303:15) +#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4027:5) +#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4286:5) +#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4461:11) +#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4281:5) +#10 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3276:14)'''; + + expect(console, isEmpty); + FrameworkError.dumpErrorToConsole( + FrameworkErrorDetails( + exception: AssertionError('Test assertion'), + stack: StackTrace.fromString(stackString), + ), + ); + final String x = console.join('\n'); + expect( + x, + startsWith( + ''' +══╡ EXCEPTION CAUGHT BY FLUTTER FRAMEWORK ╞═════════════════════════════════════════════════════════ +The following assertion was thrown: +Assertion failed: "Test assertion" + +When the exception was thrown, this was the stack: +#2 new Text (package:flutter/src/widgets/text.dart:287:10) +#3 _MyHomePageState.build (package:hello_flutter/main.dart:72:16) +#4 StatefulElement.build (package:flutter/src/widgets/framework.dart:4414:27) +#5 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4303:15) +#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4027:5) +#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4286:5) +#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4461:11) +#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4281:5)''', + ), + ); + console.clear(); + FrameworkError.resetErrorCount(); + }); +}