import 'dart:async';
import 'package:flutter/foundation.dart';
class _AsyncScope {
_AsyncScope(this.creationStack, this.zone);
final StackTrace creationStack;
final Zone zone;
}
class TestAsyncUtils {
TestAsyncUtils._();
static const String _className = 'TestAsyncUtils';
static final List<_AsyncScope> _scopeStack = <_AsyncScope>[];
static Future<T> guard<T>(Future<T> body()) {
guardSync();
final Zone zone = Zone.current.fork(
zoneValues: <dynamic, dynamic>{
_scopeStack: true,
}
);
final _AsyncScope scope = _AsyncScope(StackTrace.current, zone);
_scopeStack.add(scope);
final Future<T> result = scope.zone.run<Future<T>>(body);
T resultValue;
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;
}
static void guardSync() {
if (_scopeStack.isEmpty) {
return;
}
final Zone zone = _currentScopeZone;
if (zone == _scopeStack.last.zone) {
return;
}
int skipCount = 0;
_AsyncScope candidateScope = _scopeStack.last;
_AsyncScope scope;
do {
skipCount += 1;
scope = candidateScope;
if (skipCount >= _scopeStack.length) {
if (zone == null)
break;
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>[];
information.add(ErrorSummary('Guarded function conflict.'));
information.add(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);
}
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 {
index += 1;
assert(index < stack.length);
lineMatch = getClassPattern.matchAsPrefix(stack[index]);
assert(lineMatch != null);
assert(lineMatch.groupCount == 1);
} while (lineMatch.group(1) == _className);
if (index < stack.length) {
final RegExp guardPattern = RegExp(r'^#[0-9]+ +(?:([^. ]+)\.)?([^. ]+)');
final Match guardMatch = guardPattern.matchAsPrefix(stack[index]);
if (guardMatch != null) {
assert(guardMatch.groupCount == 2);
final String guardClass = guardMatch.group(1);
final String guardMethod = guardMatch.group(2);
while (index < stack.length) {
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]);
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 {
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;
}