• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/foundation.dart';
8
9class _AsyncScope {
10  _AsyncScope(this.creationStack, this.zone);
11  final StackTrace creationStack;
12  final Zone zone;
13}
14
15/// Utility class for all the async APIs in the `flutter_test` library.
16///
17/// This class provides checking for asynchronous APIs, allowing the library to
18/// verify that all the asynchronous APIs are properly `await`ed before calling
19/// another.
20///
21/// For example, it prevents this kind of code:
22///
23/// ```dart
24/// tester.pump(); // forgot to call "await"!
25/// tester.pump();
26/// ```
27///
28/// ...by detecting, in the second call to `pump`, that it should actually be:
29///
30/// ```dart
31/// await tester.pump();
32/// await tester.pump();
33/// ```
34///
35/// It does this while still allowing nested calls, e.g. so that you can
36/// call [expect] from inside callbacks.
37///
38/// You can use this in your own test functions, if you have some asynchronous
39/// functions that must be used with "await". Wrap the contents of the function
40/// in a call to TestAsyncUtils.guard(), as follows:
41///
42/// ```dart
43/// Future<void> myTestFunction() => TestAsyncUtils.guard(() async {
44///   // ...
45/// });
46/// ```
47class TestAsyncUtils {
48  TestAsyncUtils._();
49  static const String _className = 'TestAsyncUtils';
50
51  static final List<_AsyncScope> _scopeStack = <_AsyncScope>[];
52
53  /// Calls the given callback in a new async scope. The callback argument is
54  /// the asynchronous body of the calling method. The calling method is said to
55  /// be "guarded". Nested calls to guarded methods from within the body of this
56  /// one are fine, but calls to other guarded methods from outside the body of
57  /// this one before this one has finished will throw an exception.
58  ///
59  /// This method first calls [guardSync].
60  static Future<T> guard<T>(Future<T> body()) {
61    guardSync();
62    final Zone zone = Zone.current.fork(
63      zoneValues: <dynamic, dynamic>{
64        _scopeStack: true, // so we can recognize this as our own zone
65      }
66    );
67    final _AsyncScope scope = _AsyncScope(StackTrace.current, zone);
68    _scopeStack.add(scope);
69    final Future<T> result = scope.zone.run<Future<T>>(body);
70    T resultValue; // This is set when the body of work completes with a result value.
71    Future<T> completionHandler(dynamic error, StackTrace stack) {
72      assert(_scopeStack.isNotEmpty);
73      assert(_scopeStack.contains(scope));
74      bool leaked = false;
75      _AsyncScope closedScope;
76      final List<DiagnosticsNode> information = <DiagnosticsNode>[];
77      while (_scopeStack.isNotEmpty) {
78        closedScope = _scopeStack.removeLast();
79        if (closedScope == scope)
80          break;
81        if (!leaked) {
82          information.add(ErrorSummary('Asynchronous call to guarded function leaked.'));
83          information.add(ErrorHint('You must use "await" with all Future-returning test APIs.'));
84          leaked = true;
85        }
86        final _StackEntry originalGuarder = _findResponsibleMethod(closedScope.creationStack, 'guard', information);
87        if (originalGuarder != null) {
88          information.add(ErrorDescription(
89            'The test API method "${originalGuarder.methodName}" '
90            'from class ${originalGuarder.className} '
91            'was called from ${originalGuarder.callerFile} '
92            'on line ${originalGuarder.callerLine}, '
93            'but never completed before its parent scope closed.'
94          ));
95        }
96      }
97      if (leaked) {
98        if (error != null) {
99          information.add(DiagnosticsProperty<dynamic>(
100            'An uncaught exception may have caused the guarded function leak. The exception was',
101            error,
102            style: DiagnosticsTreeStyle.errorProperty,
103          ));
104          information.add(DiagnosticsStackTrace('The stack trace associated with this exception was', stack));
105        }
106        throw FlutterError.fromParts(information);
107      }
108      if (error != null)
109        return Future<T>.error(error, stack);
110      return Future<T>.value(resultValue);
111    }
112    return result.then<T>(
113      (T value) {
114        resultValue = value;
115        return completionHandler(null, null);
116      },
117      onError: completionHandler,
118    );
119  }
120
121  static Zone get _currentScopeZone {
122    Zone zone = Zone.current;
123    while (zone != null) {
124      if (zone[_scopeStack] == true)
125        return zone;
126      zone = zone.parent;
127    }
128    return null;
129  }
130
131  /// Verifies that there are no guarded methods currently pending (see [guard]).
132  ///
133  /// If a guarded method is currently pending, and this is not a call nested
134  /// from inside that method's body (directly or indirectly), then this method
135  /// will throw a detailed exception.
136  static void guardSync() {
137    if (_scopeStack.isEmpty) {
138      // No scopes open, so we must be fine.
139      return;
140    }
141    // Find the current TestAsyncUtils scope zone so we can see if it's the one we expect.
142    final Zone zone = _currentScopeZone;
143    if (zone == _scopeStack.last.zone) {
144      // We're still in the current scope zone. All good.
145      return;
146    }
147    // If we get here, we know we've got a conflict on our hands.
148    // We got an async barrier, but the current zone isn't the last scope that
149    // we pushed on the stack.
150    // Find which scope the conflict happened in, so that we know
151    // which stack trace to report the conflict as starting from.
152    //
153    // For example, if we called an async method A, which ran its body in a
154    // guarded block, and in its body it ran an async method B, which ran its
155    // body in a guarded block, but we didn't await B, then in A's block we ran
156    // an async method C, which ran its body in a guarded block, then we should
157    // complain about the call to B then the call to C. BUT. If we called an async
158    // method A, which ran its body in a guarded block, and in its body it ran
159    // an async method B, which ran its body in a guarded block, but we didn't
160    // await A, and then at the top level we called a method D, then we should
161    // complain about the call to A then the call to D.
162    //
163    // In both examples, the scope stack would have two scopes. In the first
164    // example, the current zone would be the zone of the _scopeStack[0] scope,
165    // and we would want to show _scopeStack[1]'s creationStack. In the second
166    // example, the current zone would not be in the _scopeStack, and we would
167    // want to show _scopeStack[0]'s creationStack.
168    int skipCount = 0;
169    _AsyncScope candidateScope = _scopeStack.last;
170    _AsyncScope scope;
171    do {
172      skipCount += 1;
173      scope = candidateScope;
174      if (skipCount >= _scopeStack.length) {
175        if (zone == null)
176          break;
177        // Some people have reported reaching this point, but it's not clear
178        // why. For now, just silently return.
179        // TODO(ianh): If we ever get a test case that shows how we reach
180        // this point, reduce it and report the error if there is one.
181        return;
182      }
183      candidateScope = _scopeStack[_scopeStack.length - skipCount - 1];
184      assert(candidateScope != null);
185      assert(candidateScope.zone != null);
186    } while (candidateScope.zone != zone);
187    assert(scope != null);
188    final List<DiagnosticsNode> information = <DiagnosticsNode>[];
189    information.add(ErrorSummary('Guarded function conflict.'));
190    information.add(ErrorHint('You must use "await" with all Future-returning test APIs.'));
191    final _StackEntry originalGuarder = _findResponsibleMethod(scope.creationStack, 'guard', information);
192    final _StackEntry collidingGuarder = _findResponsibleMethod(StackTrace.current, 'guardSync', information);
193    if (originalGuarder != null && collidingGuarder != null) {
194      String originalName;
195      if (originalGuarder.className == null) {
196        originalName = '(${originalGuarder.methodName}) ';
197        information.add(ErrorDescription(
198          'The guarded "${originalGuarder.methodName}" function '
199          'was called from ${originalGuarder.callerFile} '
200          'on line ${originalGuarder.callerLine}.'
201        ));
202      } else {
203        originalName = '(${originalGuarder.className}.${originalGuarder.methodName}) ';
204        information.add(ErrorDescription(
205          'The guarded method "${originalGuarder.methodName}" '
206          'from class ${originalGuarder.className} '
207          'was called from ${originalGuarder.callerFile} '
208          'on line ${originalGuarder.callerLine}.'
209        ));
210      }
211      final String again = (originalGuarder.callerFile == collidingGuarder.callerFile) &&
212                           (originalGuarder.callerLine == collidingGuarder.callerLine) ?
213                           'again ' : '';
214      String collidingName;
215      if ((originalGuarder.className == collidingGuarder.className) &&
216          (originalGuarder.methodName == collidingGuarder.methodName)) {
217        originalName = '';
218        collidingName = '';
219        information.add(ErrorDescription(
220          'Then, it '
221          'was called ${again}from ${collidingGuarder.callerFile} '
222          'on line ${collidingGuarder.callerLine}.'
223        ));
224      } else if (collidingGuarder.className == null) {
225        collidingName = '(${collidingGuarder.methodName}) ';
226        information.add(ErrorDescription(
227          'Then, the "${collidingGuarder.methodName}" function '
228          'was called ${again}from ${collidingGuarder.callerFile} '
229          'on line ${collidingGuarder.callerLine}.'
230        ));
231      } else {
232        collidingName = '(${collidingGuarder.className}.${collidingGuarder.methodName}) ';
233        information.add(ErrorDescription(
234          'Then, the "${collidingGuarder.methodName}" method '
235          '${originalGuarder.className == collidingGuarder.className ? "(also from class ${collidingGuarder.className})"
236                                                                     : "from class ${collidingGuarder.className}"} '
237          'was called ${again}from ${collidingGuarder.callerFile} '
238          'on line ${collidingGuarder.callerLine}.'
239        ));
240      }
241      information.add(ErrorDescription(
242        'The first ${originalGuarder.className == null ? "function" : "method"} $originalName'
243        'had not yet finished executing at the time that '
244        'the second ${collidingGuarder.className == null ? "function" : "method"} $collidingName'
245        'was called. Since both are guarded, and the second was not a nested call inside the first, the '
246        'first must complete its execution before the second can be called. Typically, this is achieved by '
247        'putting an "await" statement in front of the call to the first.'
248      ));
249      if (collidingGuarder.className == null && collidingGuarder.methodName == 'expect') {
250        information.add(ErrorHint(
251          'If you are confident that all test APIs are being called using "await", and '
252          'this expect() call is not being called at the top level but is itself being '
253          'called from some sort of callback registered before the ${originalGuarder.methodName} '
254          'method was called, then consider using expectSync() instead.'
255        ));
256      }
257      information.add(DiagnosticsStackTrace(
258        '\nWhen the first ${originalGuarder.className == null ? "function" : "method"} '
259        '$originalName'
260        'was called, this was the stack',
261        scope.creationStack,
262      ));
263    }
264    throw FlutterError.fromParts(information);
265  }
266
267  /// Verifies that there are no guarded methods currently pending (see [guard]).
268  ///
269  /// This is used at the end of tests to ensure that nothing leaks out of the test.
270  static void verifyAllScopesClosed() {
271    if (_scopeStack.isNotEmpty) {
272      final List<DiagnosticsNode> information = <DiagnosticsNode>[
273        ErrorSummary('Asynchronous call to guarded function leaked.'),
274        ErrorHint('You must use "await" with all Future-returning test APIs.')
275      ];
276      for (_AsyncScope scope in _scopeStack) {
277        final _StackEntry guarder = _findResponsibleMethod(scope.creationStack, 'guard', information);
278        if (guarder != null) {
279          information.add(ErrorDescription(
280            'The guarded method "${guarder.methodName}" '
281            '${guarder.className != null ? "from class ${guarder.className} " : ""}'
282            'was called from ${guarder.callerFile} '
283            'on line ${guarder.callerLine}, '
284            'but never completed before its parent scope closed.'
285          ));
286        }
287      }
288      throw FlutterError.fromParts(information);
289    }
290  }
291
292  static bool _stripAsynchronousSuspensions(String line) {
293    return line != '<asynchronous suspension>';
294  }
295
296  static _StackEntry _findResponsibleMethod(StackTrace rawStack, String method, List<DiagnosticsNode> information) {
297    assert(method == 'guard' || method == 'guardSync');
298    final List<String> stack = rawStack.toString().split('\n').where(_stripAsynchronousSuspensions).toList();
299    assert(stack.last == '');
300    stack.removeLast();
301    final RegExp getClassPattern = RegExp(r'^#[0-9]+ +([^. ]+)');
302    Match lineMatch;
303    int index = -1;
304    do { // skip past frames that are from this class
305      index += 1;
306      assert(index < stack.length);
307      lineMatch = getClassPattern.matchAsPrefix(stack[index]);
308      assert(lineMatch != null);
309      assert(lineMatch.groupCount == 1);
310    } while (lineMatch.group(1) == _className);
311    // try to parse the stack to find the interesting frame
312    if (index < stack.length) {
313      final RegExp guardPattern = RegExp(r'^#[0-9]+ +(?:([^. ]+)\.)?([^. ]+)');
314      final Match guardMatch = guardPattern.matchAsPrefix(stack[index]); // find the class that called us
315      if (guardMatch != null) {
316        assert(guardMatch.groupCount == 2);
317        final String guardClass = guardMatch.group(1); // might be null
318        final String guardMethod = guardMatch.group(2);
319        while (index < stack.length) { // find the last stack frame that called the class that called us
320          lineMatch = getClassPattern.matchAsPrefix(stack[index]);
321          if (lineMatch != null) {
322            assert(lineMatch.groupCount == 1);
323            if (lineMatch.group(1) == (guardClass ?? guardMethod)) {
324              index += 1;
325              continue;
326            }
327          }
328          break;
329        }
330        if (index < stack.length) {
331          final RegExp callerPattern = RegExp(r'^#[0-9]+ .* \((.+?):([0-9]+)(?::[0-9]+)?\)$');
332          final Match callerMatch = callerPattern.matchAsPrefix(stack[index]); // extract the caller's info
333          if (callerMatch != null) {
334            assert(callerMatch.groupCount == 2);
335            final String callerFile = callerMatch.group(1);
336            final String callerLine = callerMatch.group(2);
337            return _StackEntry(guardClass, guardMethod, callerFile, callerLine);
338          } else {
339            // One reason you might get here is if the guarding method was called directly from
340            // a 'dart:' API, like from the Future/microtask mechanism, because dart: URLs in the
341            // stack trace don't have a column number and so don't match the regexp above.
342            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.)'));
343            information.add(ErrorDescription('${stack[index]}'));
344          }
345        } else {
346          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.)'));
347        }
348      } else {
349        information.add(ErrorSummary('(Unable to parse the stack frame of the method that called $_className.$method(). The stack may be incomplete or bogus.)'));
350        information.add(ErrorDescription('${stack[index]}'));
351      }
352    } else {
353      information.add(ErrorSummary('(Unable to find the method that called $_className.$method(). The stack may be incomplete or bogus.)'));
354    }
355    return null;
356  }
357}
358
359class _StackEntry {
360  const _StackEntry(this.className, this.methodName, this.callerFile, this.callerLine);
361  final String className;
362  final String methodName;
363  final String callerFile;
364  final String callerLine;
365}
366