| // 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. | |
| /// @docImport 'widget_tester.dart'; | |
| library; | |
| import 'dart:async'; | |
| import 'package:flutter/foundation.dart'; | |
| class _AsyncScope { | |
| _AsyncScope(this.creationStack, this.zone); | |
| final StackTrace creationStack; | |
| final Zone zone; | |
| } | |
| // Examples can assume: | |
| // late WidgetTester tester; | |
| /// Utility class for all the async APIs in the `flutter_test` library. | |
| /// | |
| /// This class provides checking for asynchronous APIs, allowing the library to | |
| /// verify that all the asynchronous APIs are properly `await`ed before calling | |
| /// another. | |
| /// | |
| /// For example, it prevents this kind of code: | |
| /// | |
| /// ```dart | |
| /// tester.pump(); // forgot to call "await"! | |
| /// tester.pump(); | |
| /// ``` | |
| /// | |
| /// ...by detecting, in the second call to `pump`, that it should actually be: | |
| /// | |
| /// ```dart | |
| /// await tester.pump(); | |
| /// await tester.pump(); | |
| /// ``` | |
| /// | |
| /// It does this while still allowing nested calls, e.g. so that you can | |
| /// call [expect] from inside callbacks. | |
| /// | |
| /// You can use this in your own test functions, if you have some asynchronous | |
| /// functions that must be used with "await". Wrap the contents of the function | |
| /// in a call to TestAsyncUtils.guard(), as follows: | |
| /// | |
| /// ```dart | |
| /// Future<void> myTestFunction() => TestAsyncUtils.guard(() async { | |
| /// // ... | |
| /// }); | |
| /// ``` | |
| abstract final class TestAsyncUtils { | |
| static const String _className = 'TestAsyncUtils'; | |
| static final List<_AsyncScope> _scopeStack = <_AsyncScope>[]; | |
| /// Calls the given callback in a new async scope. The callback argument is | |
| /// the asynchronous body of the calling method. The calling method is said to | |
| /// be "guarded". Nested calls to guarded methods from within the body of this | |
| /// one are fine, but calls to other guarded methods from outside the body of | |
| /// this one before this one has finished will throw an exception. | |
| /// | |
| /// This method first calls [guardSync]. | |
| static Future<T> guard<T>(Future<T> Function() body) { | |
| guardSync(); | |
| final Zone zone = Zone.current.fork( | |
| zoneValues: <dynamic, dynamic>{ | |
| _scopeStack: true, // so we can recognize this as our own zone | |
| }, | |
| ); | |
| final scope = _AsyncScope(StackTrace.current, zone); | |
| _scopeStack.add(scope); | |
| final Future<T> result = scope.zone.run<Future<T>>(body); | |
| late T resultValue; // This is set when the body of work completes with a result value. | |
| Future<T> completionHandler(dynamic error, StackTrace? stack) { | |
| assert(_scopeStack.isNotEmpty); | |
| assert(_scopeStack.contains(scope)); | |
| var leaked = false; | |
| _AsyncScope closedScope; | |
| final information = <DiagnosticsNode>[]; | |
| while (_scopeStack.isNotEmpty) { | |
| closedScope = _scopeStack.removeLast(); | |
| if (closedScope == scope) { | |
| break; | |
| } | |
| if (!leaked) { | |
| information.add(ErrorSummary('Asynchronous call to guarded function leaked.')); | |
| information.add(ErrorHint('You must use "await" with all Future-returning test APIs.')); | |
| leaked = true; | |
| } | |
| final _StackEntry? originalGuarder = _findResponsibleMethod( | |
| closedScope.creationStack, | |
| 'guard', | |
| information, | |
| ); | |
| if (originalGuarder != null) { | |
| information.add( | |
| ErrorDescription( | |
| 'The test API method "${originalGuarder.methodName}" ' | |
| 'from class ${originalGuarder.className} ' | |
| 'was called from ${originalGuarder.callerFile} ' | |
| 'on line ${originalGuarder.callerLine}, ' | |
| 'but never completed before its parent scope closed.', | |
| ), | |
| ); | |
| } | |
| } | |
| if (leaked) { | |
| if (error != null) { | |
| information.add( | |
| DiagnosticsProperty<dynamic>( | |
| 'An uncaught exception may have caused the guarded function leak. The exception was', | |
| error, | |
| style: DiagnosticsTreeStyle.errorProperty, | |
| ), | |
| ); | |
| information.add( | |
| DiagnosticsStackTrace('The stack trace associated with this exception was', stack), | |
| ); | |
| } | |
| throw FlutterError.fromParts(information); | |
| } | |
| if (error != null) { | |
| return Future<T>.error(error! as Object, stack); | |
| } | |
| return Future<T>.value(resultValue); | |
| } | |
| return result.then<T>((T value) { | |
| resultValue = value; | |
| return completionHandler(null, null); | |
| }, onError: completionHandler); | |
| } | |
| static Zone? get _currentScopeZone { | |
| Zone? zone = Zone.current; | |
| while (zone != null) { | |
| if (zone[_scopeStack] == true) { | |
| return zone; | |
| } | |
| zone = zone.parent; | |
| } | |
| return null; | |
| } | |
| /// Verifies that there are no guarded methods currently pending (see [guard]). | |
| /// | |
| /// If a guarded method is currently pending, and this is not a call nested | |
| /// from inside that method's body (directly or indirectly), then this method | |
| /// will throw a detailed exception. | |
| static void guardSync() { | |
| if (_scopeStack.isEmpty) { | |
| // No scopes open, so we must be fine. | |
| return; | |
| } | |
| // Find the current TestAsyncUtils scope zone so we can see if it's the one we expect. | |
| final Zone? zone = _currentScopeZone; | |
| if (zone == _scopeStack.last.zone) { | |
| // We're still in the current scope zone. All good. | |
| return; | |
| } | |
| // If we get here, we know we've got a conflict on our hands. | |
| // We got an async barrier, but the current zone isn't the last scope that | |
| // we pushed on the stack. | |
| // Find which scope the conflict happened in, so that we know | |
| // which stack trace to report the conflict as starting from. | |
| // | |
| // For example, if we called an async method A, which ran its body in a | |
| // guarded block, and in its body it ran an async method B, which ran its | |
| // body in a guarded block, but we didn't await B, then in A's block we ran | |
| // an async method C, which ran its body in a guarded block, then we should | |
| // complain about the call to B then the call to C. BUT. If we called an async | |
| // method A, which ran its body in a guarded block, and in its body it ran | |
| // an async method B, which ran its body in a guarded block, but we didn't | |
| // await A, and then at the top level we called a method D, then we should | |
| // complain about the call to A then the call to D. | |
| // | |
| // In both examples, the scope stack would have two scopes. In the first | |
| // example, the current zone would be the zone of the _scopeStack[0] scope, | |
| // and we would want to show _scopeStack[1]'s creationStack. In the second | |
| // example, the current zone would not be in the _scopeStack, and we would | |
| // want to show _scopeStack[0]'s creationStack. | |
| var skipCount = 0; | |
| _AsyncScope candidateScope = _scopeStack.last; | |
| _AsyncScope scope; | |
| do { | |
| skipCount += 1; | |
| scope = candidateScope; | |
| if (skipCount >= _scopeStack.length) { | |
| if (zone == null) { | |
| break; | |
| } | |
| // Some people have reported reaching this point, but it's not clear | |
| // why. For now, just silently return. | |
| // TODO(ianh): If we ever get a test case that shows how we reach | |
| // this point, reduce it and report the error if there is one. | |
| return; | |
| } | |
| candidateScope = _scopeStack[_scopeStack.length - skipCount - 1]; | |
| } while (candidateScope.zone != zone); | |
| final information = <DiagnosticsNode>[ | |
| ErrorSummary('Guarded function conflict.'), | |
| ErrorHint('You must use "await" with all Future-returning test APIs.'), | |
| ]; | |
| final _StackEntry? originalGuarder = _findResponsibleMethod( | |
| scope.creationStack, | |
| 'guard', | |
| information, | |
| ); | |
| final _StackEntry? collidingGuarder = _findResponsibleMethod( | |
| StackTrace.current, | |
| 'guardSync', | |
| information, | |
| ); | |
| if (originalGuarder != null && collidingGuarder != null) { | |
| final originalKind = originalGuarder.className == null ? 'function' : 'method'; | |
| String originalName; | |
| if (originalGuarder.className == null) { | |
| originalName = '$originalKind (${originalGuarder.methodName})'; | |
| information.add( | |
| ErrorDescription( | |
| 'The guarded "${originalGuarder.methodName}" function ' | |
| 'was called from ${originalGuarder.callerFile} ' | |
| 'on line ${originalGuarder.callerLine}.', | |
| ), | |
| ); | |
| } else { | |
| originalName = '$originalKind (${originalGuarder.className}.${originalGuarder.methodName})'; | |
| information.add( | |
| ErrorDescription( | |
| 'The guarded method "${originalGuarder.methodName}" ' | |
| 'from class ${originalGuarder.className} ' | |
| 'was called from ${originalGuarder.callerFile} ' | |
| 'on line ${originalGuarder.callerLine}.', | |
| ), | |
| ); | |
| } | |
| final again = | |
| (originalGuarder.callerFile == collidingGuarder.callerFile) && | |
| (originalGuarder.callerLine == collidingGuarder.callerLine) | |
| ? 'again ' | |
| : ''; | |
| final collidingKind = collidingGuarder.className == null ? 'function' : 'method'; | |
| String collidingName; | |
| if ((originalGuarder.className == collidingGuarder.className) && | |
| (originalGuarder.methodName == collidingGuarder.methodName)) { | |
| originalName = originalKind; | |
| collidingName = collidingKind; | |
| information.add( | |
| ErrorDescription( | |
| 'Then, it ' | |
| 'was called ${again}from ${collidingGuarder.callerFile} ' | |
| 'on line ${collidingGuarder.callerLine}.', | |
| ), | |
| ); | |
| } else if (collidingGuarder.className == null) { | |
| collidingName = '$collidingKind (${collidingGuarder.methodName})'; | |
| information.add( | |
| ErrorDescription( | |
| 'Then, the "${collidingGuarder.methodName}" function ' | |
| 'was called ${again}from ${collidingGuarder.callerFile} ' | |
| 'on line ${collidingGuarder.callerLine}.', | |
| ), | |
| ); | |
| } else { | |
| collidingName = | |
| '$collidingKind (${collidingGuarder.className}.${collidingGuarder.methodName})'; | |
| information.add( | |
| ErrorDescription( | |
| 'Then, the "${collidingGuarder.methodName}" method ' | |
| '${originalGuarder.className == collidingGuarder.className ? "(also from class ${collidingGuarder.className})" : "from class ${collidingGuarder.className}"} ' | |
| 'was called ${again}from ${collidingGuarder.callerFile} ' | |
| 'on line ${collidingGuarder.callerLine}.', | |
| ), | |
| ); | |
| } | |
| information.add( | |
| ErrorDescription( | |
| 'The first $originalName ' | |
| 'had not yet finished executing at the time that ' | |
| 'the second $collidingName ' | |
| 'was called. Since both are guarded, and the second was not a nested call inside the first, the ' | |
| 'first must complete its execution before the second can be called. Typically, this is achieved by ' | |
| 'putting an "await" statement in front of the call to the first.', | |
| ), | |
| ); | |
| if (collidingGuarder.className == null && collidingGuarder.methodName == 'expect') { | |
| information.add( | |
| ErrorHint( | |
| 'If you are confident that all test APIs are being called using "await", and ' | |
| 'this expect() call is not being called at the top level but is itself being ' | |
| 'called from some sort of callback registered before the ${originalGuarder.methodName} ' | |
| 'method was called, then consider using expectSync() instead.', | |
| ), | |
| ); | |
| } | |
| information.add( | |
| DiagnosticsStackTrace( | |
| '\nWhen the first $originalName was called, this was the stack', | |
| scope.creationStack, | |
| ), | |
| ); | |
| } else { | |
| information.add( | |
| DiagnosticsStackTrace( | |
| '\nWhen the first function was called, this was the stack', | |
| scope.creationStack, | |
| ), | |
| ); | |
| } | |
| throw FlutterError.fromParts(information); | |
| } | |
| /// Verifies that there are no guarded methods currently pending (see [guard]). | |
| /// | |
| /// This is used at the end of tests to ensure that nothing leaks out of the test. | |
| static void verifyAllScopesClosed() { | |
| if (_scopeStack.isNotEmpty) { | |
| final information = <DiagnosticsNode>[ | |
| ErrorSummary('Asynchronous call to guarded function leaked.'), | |
| ErrorHint('You must use "await" with all Future-returning test APIs.'), | |
| ]; | |
| for (final _AsyncScope scope in _scopeStack) { | |
| final _StackEntry? guarder = _findResponsibleMethod( | |
| scope.creationStack, | |
| 'guard', | |
| information, | |
| ); | |
| if (guarder != null) { | |
| information.add( | |
| ErrorDescription( | |
| 'The guarded method "${guarder.methodName}" ' | |
| '${guarder.className != null ? "from class ${guarder.className} " : ""}' | |
| 'was called from ${guarder.callerFile} ' | |
| 'on line ${guarder.callerLine}, ' | |
| 'but never completed before its parent scope closed.', | |
| ), | |
| ); | |
| } | |
| } | |
| throw FlutterError.fromParts(information); | |
| } | |
| } | |
| static bool _stripAsynchronousSuspensions(String line) { | |
| return line != '<asynchronous suspension>'; | |
| } | |
| static _StackEntry? _findResponsibleMethod( | |
| StackTrace rawStack, | |
| String method, | |
| List<DiagnosticsNode> information, | |
| ) { | |
| assert(method == 'guard' || method == 'guardSync'); | |
| // Web/JavaScript stack traces use a different format. | |
| if (kIsWeb) { | |
| return null; | |
| } | |
| final List<String> stack = rawStack | |
| .toString() | |
| .split('\n') | |
| .where(_stripAsynchronousSuspensions) | |
| .toList(); | |
| assert(stack.last == ''); | |
| stack.removeLast(); | |
| final getClassPattern = RegExp(r'^#[0-9]+ +([^. ]+)'); | |
| Match? lineMatch; | |
| var index = -1; | |
| do { | |
| // skip past frames that are from this class | |
| index += 1; | |
| assert(index < stack.length); | |
| lineMatch = getClassPattern.matchAsPrefix(stack[index]); | |
| assert(lineMatch != null); | |
| lineMatch = lineMatch!; | |
| assert(lineMatch.groupCount == 1); | |
| } while (lineMatch.group(1) == _className); | |
| // try to parse the stack to find the interesting frame | |
| if (index < stack.length) { | |
| final guardPattern = RegExp(r'^#[0-9]+ +(?:([^. ]+)\.)?([^. ]+)'); | |
| final Match? guardMatch = guardPattern.matchAsPrefix( | |
| stack[index], | |
| ); // find the class that called us | |
| if (guardMatch != null) { | |
| assert(guardMatch.groupCount == 2); | |
| final String? guardClass = guardMatch.group(1); // might be null | |
| final String? guardMethod = guardMatch.group(2); | |
| while (index < stack.length) { | |
| // find the last stack frame that called the class that called us | |
| lineMatch = getClassPattern.matchAsPrefix(stack[index]); | |
| if (lineMatch != null) { | |
| assert(lineMatch.groupCount == 1); | |
| if (lineMatch.group(1) == (guardClass ?? guardMethod)) { | |
| index += 1; | |
| continue; | |
| } | |
| } | |
| break; | |
| } | |
| if (index < stack.length) { | |
| final callerPattern = RegExp(r'^#[0-9]+ .* \((.+?):([0-9]+)(?::[0-9]+)?\)$'); | |
| final Match? callerMatch = callerPattern.matchAsPrefix( | |
| stack[index], | |
| ); // extract the caller's info | |
| if (callerMatch != null) { | |
| assert(callerMatch.groupCount == 2); | |
| final String? callerFile = callerMatch.group(1); | |
| final String? callerLine = callerMatch.group(2); | |
| return _StackEntry(guardClass, guardMethod, callerFile, callerLine); | |
| } else { | |
| // One reason you might get here is if the guarding method was called directly from | |
| // a 'dart:' API, like from the Future/microtask mechanism, because dart: URLs in the | |
| // stack trace don't have a column number and so don't match the regexp above. | |
| information.add( | |
| ErrorSummary( | |
| '(Unable to parse the stack frame of the method that called the method that called $_className.$method(). The stack may be incomplete or bogus.)', | |
| ), | |
| ); | |
| information.add(ErrorDescription(stack[index])); | |
| } | |
| } else { | |
| information.add( | |
| ErrorSummary( | |
| '(Unable to find the stack frame of the method that called the method that called $_className.$method(). The stack may be incomplete or bogus.)', | |
| ), | |
| ); | |
| } | |
| } else { | |
| information.add( | |
| ErrorSummary( | |
| '(Unable to parse the stack frame of the method that called $_className.$method(). The stack may be incomplete or bogus.)', | |
| ), | |
| ); | |
| information.add(ErrorDescription(stack[index])); | |
| } | |
| } else { | |
| information.add( | |
| ErrorSummary( | |
| '(Unable to find the method that called $_className.$method(). The stack may be incomplete or bogus.)', | |
| ), | |
| ); | |
| } | |
| return null; | |
| } | |
| } | |
| class _StackEntry { | |
| const _StackEntry(this.className, this.methodName, this.callerFile, this.callerLine); | |
| final String? className; | |
| final String? methodName; | |
| final String? callerFile; | |
| final String? callerLine; | |
| } |