mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

* Update project.pbxproj files to say Flutter rather than Chromium Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright. * Update the copyright notice checker to require a standard notice on all files * Update copyrights on Dart files. (This was a mechanical commit.) * Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine. Some were already marked "The Flutter Authors", not clear why. Their dates have been normalized. Some were missing the blank line after the license. Some were randomly different in trivial ways for no apparent reason (e.g. missing the trailing period). * Clean up the copyrights in non-Dart files. (Manual edits.) Also, make sure templates don't have copyrights. * Fix some more ORGANIZATIONNAMEs
367 lines
16 KiB
Dart
367 lines
16 KiB
Dart
// 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 'package:flutter/foundation.dart';
|
|
|
|
class _AsyncScope {
|
|
_AsyncScope(this.creationStack, this.zone);
|
|
final StackTrace creationStack;
|
|
final Zone zone;
|
|
}
|
|
|
|
/// 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 {
|
|
/// // ...
|
|
/// });
|
|
/// ```
|
|
class TestAsyncUtils {
|
|
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> body()) {
|
|
guardSync();
|
|
final Zone zone = Zone.current.fork(
|
|
zoneValues: <dynamic, dynamic>{
|
|
_scopeStack: true, // so we can recognize this as our own zone
|
|
}
|
|
);
|
|
final _AsyncScope scope = _AsyncScope(StackTrace.current, zone);
|
|
_scopeStack.add(scope);
|
|
final Future<T> result = scope.zone.run<Future<T>>(body);
|
|
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));
|
|
bool leaked = false;
|
|
_AsyncScope closedScope;
|
|
final List<DiagnosticsNode> 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, 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.
|
|
int 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];
|
|
assert(candidateScope != null);
|
|
assert(candidateScope.zone != null);
|
|
} while (candidateScope.zone != zone);
|
|
assert(scope != null);
|
|
final List<DiagnosticsNode> 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) {
|
|
String originalName;
|
|
if (originalGuarder.className == null) {
|
|
originalName = '(${originalGuarder.methodName}) ';
|
|
information.add(ErrorDescription(
|
|
'The guarded "${originalGuarder.methodName}" function '
|
|
'was called from ${originalGuarder.callerFile} '
|
|
'on line ${originalGuarder.callerLine}.'
|
|
));
|
|
} else {
|
|
originalName = '(${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 String again = (originalGuarder.callerFile == collidingGuarder.callerFile) &&
|
|
(originalGuarder.callerLine == collidingGuarder.callerLine) ?
|
|
'again ' : '';
|
|
String collidingName;
|
|
if ((originalGuarder.className == collidingGuarder.className) &&
|
|
(originalGuarder.methodName == collidingGuarder.methodName)) {
|
|
originalName = '';
|
|
collidingName = '';
|
|
information.add(ErrorDescription(
|
|
'Then, it '
|
|
'was called ${again}from ${collidingGuarder.callerFile} '
|
|
'on line ${collidingGuarder.callerLine}.'
|
|
));
|
|
} else if (collidingGuarder.className == null) {
|
|
collidingName = '(${collidingGuarder.methodName}) ';
|
|
information.add(ErrorDescription(
|
|
'Then, the "${collidingGuarder.methodName}" function '
|
|
'was called ${again}from ${collidingGuarder.callerFile} '
|
|
'on line ${collidingGuarder.callerLine}.'
|
|
));
|
|
} else {
|
|
collidingName = '(${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 ${originalGuarder.className == null ? "function" : "method"} $originalName'
|
|
'had not yet finished executing at the time that '
|
|
'the second ${collidingGuarder.className == null ? "function" : "method"} $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 ${originalGuarder.className == null ? "function" : "method"} '
|
|
'$originalName'
|
|
'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 List<DiagnosticsNode> information = <DiagnosticsNode>[
|
|
ErrorSummary('Asynchronous call to guarded function leaked.'),
|
|
ErrorHint('You must use "await" with all Future-returning test APIs.')
|
|
];
|
|
for (_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');
|
|
final List<String> stack = rawStack.toString().split('\n').where(_stripAsynchronousSuspensions).toList();
|
|
assert(stack.last == '');
|
|
stack.removeLast();
|
|
final RegExp getClassPattern = RegExp(r'^#[0-9]+ +([^. ]+)');
|
|
Match lineMatch;
|
|
int 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);
|
|
assert(lineMatch.groupCount == 1);
|
|
} while (lineMatch.group(1) == _className);
|
|
// try to parse the stack to find the interesting frame
|
|
if (index < stack.length) {
|
|
final RegExp 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 RegExp 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;
|
|
}
|