LCOV - code coverage report
Current view: top level - test_api-0.4.8/lib/src/backend - invoker.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 88 135 65.2 %
Date: 2021-11-28 14:37:50 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
       2             : // for details. All rights reserved. Use of this source code is governed by a
       3             : // BSD-style license that can be found in the LICENSE file.
       4             : 
       5             : import 'dart:async';
       6             : 
       7             : import 'package:stack_trace/stack_trace.dart';
       8             : 
       9             : import 'closed_exception.dart';
      10             : import 'declarer.dart';
      11             : import 'group.dart';
      12             : import 'live_test.dart';
      13             : import 'live_test_controller.dart';
      14             : import 'message.dart';
      15             : import 'metadata.dart';
      16             : import 'state.dart';
      17             : import 'suite.dart';
      18             : import 'suite_platform.dart';
      19             : import 'test.dart';
      20             : import 'test_failure.dart';
      21             : import 'util/pretty_print.dart';
      22             : 
      23             : /// A test in this isolate.
      24             : class LocalTest extends Test {
      25             :   @override
      26             :   final String name;
      27             : 
      28             :   @override
      29             :   final Metadata metadata;
      30             : 
      31             :   @override
      32             :   final Trace? trace;
      33             : 
      34             :   /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
      35             :   final bool isScaffoldAll;
      36             : 
      37             :   /// The test body.
      38             :   final Function() _body;
      39             : 
      40             :   /// Whether the test is run in its own error zone.
      41             :   final bool _guarded;
      42             : 
      43             :   /// Creates a new [LocalTest].
      44             :   ///
      45             :   /// If [guarded] is `true`, the test is run in its own error zone, and any
      46             :   /// errors that escape that zone cause the test to fail. If it's `false`, it's
      47             :   /// the caller's responsiblity to invoke [LiveTest.run] in the context of a
      48             :   /// call to [Invoker.guard].
      49          11 :   LocalTest(this.name, this.metadata, this._body,
      50             :       {this.trace, bool guarded = true, this.isScaffoldAll = false})
      51             :       : _guarded = guarded;
      52             : 
      53          11 :   LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded,
      54             :       this.isScaffoldAll);
      55             : 
      56             :   /// Loads a single runnable instance of this test.
      57          11 :   @override
      58             :   LiveTest load(Suite suite, {Iterable<Group>? groups}) {
      59          22 :     var invoker = Invoker._(suite, this, groups: groups, guarded: _guarded);
      60          11 :     return invoker.liveTest;
      61             :   }
      62             : 
      63          11 :   @override
      64             :   Test? forPlatform(SuitePlatform platform) {
      65          33 :     if (!metadata.testOn.evaluate(platform)) return null;
      66          66 :     return LocalTest._(name, metadata.forPlatform(platform), _body, trace,
      67          22 :         _guarded, isScaffoldAll);
      68             :   }
      69             : }
      70             : 
      71             : /// The class responsible for managing the lifecycle of a single local test.
      72             : ///
      73             : /// The current invoker is accessible within the zone scope of the running test
      74             : /// using [Invoker.current]. It's used to track asynchronous callbacks and
      75             : /// report asynchronous errors.
      76             : class Invoker {
      77             :   /// The live test being driven by the invoker.
      78             :   ///
      79             :   /// This provides a view into the state of the test being executed.
      80          22 :   LiveTest get liveTest => _controller;
      81             :   late final LiveTestController _controller;
      82             : 
      83             :   /// Whether to run this test in its own error zone.
      84             :   final bool _guarded;
      85             : 
      86             :   /// Whether the user code is allowed to interact with the invoker despite it
      87             :   /// being closed.
      88             :   ///
      89             :   /// A test is generally closed because the runner is shutting down (in
      90             :   /// response to a signal) or because the test's suite is finished.
      91             :   /// Typically calls to [addTearDown] and [addOutstandingCallback] are only
      92             :   /// allowed before the test is closed. Tear down callbacks, however, are
      93             :   /// allowed to perform these interactions to facilitate resource cleanup on a
      94             :   /// best-effort basis, so the invoker is made to appear open only within the
      95             :   /// zones running the teardown callbacks.
      96          24 :   bool get _forceOpen => Zone.current[_forceOpenForTearDownKey] as bool;
      97             : 
      98             :   /// An opaque object used as a key in the zone value map to identify
      99             :   /// [_forceOpen].
     100             :   ///
     101             :   /// This is an instance variable to ensure that multiple invokers don't step
     102             :   /// on one anothers' toes.
     103             :   final _forceOpenForTearDownKey = Object();
     104             : 
     105             :   /// Whether the test has been closed.
     106             :   ///
     107             :   /// Once the test is closed, [expect] and [expectAsync] will throw
     108             :   /// [ClosedException]s whenever accessed to help the test stop executing as
     109             :   /// soon as possible.
     110          24 :   bool get closed => !_forceOpen && _onCloseCompleter.isCompleted;
     111             : 
     112             :   /// A future that completes once the test has been closed.
     113           0 :   Future<void> get onClose => _onCloseCompleter.future;
     114             :   final _onCloseCompleter = Completer<void>();
     115             : 
     116             :   /// The test being run.
     117          33 :   LocalTest get _test => liveTest.test as LocalTest;
     118             : 
     119             :   /// The outstanding callback counter for the current zone.
     120           6 :   _AsyncCounter get _outstandingCallbacks {
     121          18 :     var counter = Zone.current[_counterKey] as _AsyncCounter?;
     122             :     if (counter != null) return counter;
     123           0 :     throw StateError("Can't add or remove outstanding callbacks outside "
     124             :         'of a test body.');
     125             :   }
     126             : 
     127             :   /// All the zones created by [_waitForOutstandingCallbacks], in the order they
     128             :   /// were created.
     129             :   ///
     130             :   /// This is used to throw timeout errors in the most recent zone.
     131             :   final _outstandingCallbackZones = <Zone>[];
     132             : 
     133             :   /// An opaque object used as a key in the zone value map to identify
     134             :   /// [_outstandingCallbacks].
     135             :   ///
     136             :   /// This is an instance variable to ensure that multiple invokers don't step
     137             :   /// on one anothers' toes.
     138             :   final _counterKey = Object();
     139             : 
     140             :   /// The number of times this [liveTest] has been run.
     141             :   int _runCount = 0;
     142             : 
     143             :   /// The current invoker, or `null` if none is defined.
     144             :   ///
     145             :   /// An invoker is only set within the zone scope of a running test.
     146          11 :   static Invoker? get current {
     147             :     // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
     148          22 :     return Zone.current[#test.invoker] as Invoker?;
     149             :   }
     150             : 
     151             :   /// Runs [callback] in a zone where unhandled errors from [LiveTest]s are
     152             :   /// caught and dispatched to the appropriate [Invoker].
     153          11 :   static T? guard<T>(T Function() callback) =>
     154          22 :       runZoned<T?>(callback, zoneSpecification: ZoneSpecification(
     155             :           // Use [handleUncaughtError] rather than [onError] so we can
     156             :           // capture [zone] and with it the outstanding callback counter for
     157             :           // the zone in which [error] was thrown.
     158           0 :           handleUncaughtError: (self, _, zone, error, stackTrace) {
     159           0 :         var invoker = zone[#test.invoker] as Invoker?;
     160             :         if (invoker != null) {
     161           0 :           self.parent!.run(() => invoker._handleError(zone, error, stackTrace));
     162             :         } else {
     163           0 :           self.parent!.handleUncaughtError(error, stackTrace);
     164             :         }
     165             :       }));
     166             : 
     167             :   /// The timer for tracking timeouts.
     168             :   ///
     169             :   /// This will be `null` until the test starts running.
     170             :   Timer? _timeoutTimer;
     171             : 
     172             :   /// The tear-down functions to run when this test finishes.
     173             :   final _tearDowns = <Function()>[];
     174             : 
     175             :   /// Messages to print if and when this test fails.
     176             :   final _printsOnFailure = <String>[];
     177             : 
     178          11 :   Invoker._(Suite suite, LocalTest test,
     179             :       {Iterable<Group>? groups, bool guarded = true})
     180             :       : _guarded = guarded {
     181          22 :     _controller = LiveTestController(
     182          33 :         suite, test, _onRun, _onCloseCompleter.complete,
     183             :         groups: groups);
     184             :   }
     185             : 
     186             :   /// Runs [callback] after this test completes.
     187             :   ///
     188             :   /// The [callback] may return a [Future]. Like all tear-downs, callbacks are
     189             :   /// run in the reverse of the order they're declared.
     190           3 :   void addTearDown(dynamic Function() callback) {
     191           3 :     if (closed) throw ClosedException();
     192             : 
     193           6 :     if (_test.isScaffoldAll) {
     194           0 :       Declarer.current!.addTearDownAll(callback);
     195             :     } else {
     196           6 :       _tearDowns.add(callback);
     197             :     }
     198             :   }
     199             : 
     200             :   /// Tells the invoker that there's a callback running that it should wait for
     201             :   /// before considering the test successful.
     202             :   ///
     203             :   /// Each call to [addOutstandingCallback] should be followed by a call to
     204             :   /// [removeOutstandingCallback] once the callback is no longer running. Note
     205             :   /// that only successful tests wait for outstanding callbacks; as soon as a
     206             :   /// test experiences an error, any further calls to [addOutstandingCallback]
     207             :   /// or [removeOutstandingCallback] will do nothing.
     208             :   ///
     209             :   /// Throws a [ClosedException] if this test has been closed.
     210           6 :   void addOutstandingCallback() {
     211           6 :     if (closed) throw ClosedException();
     212          12 :     _outstandingCallbacks.increment();
     213             :   }
     214             : 
     215             :   /// Tells the invoker that a callback declared with [addOutstandingCallback]
     216             :   /// is no longer running.
     217           6 :   void removeOutstandingCallback() {
     218           6 :     heartbeat();
     219          12 :     _outstandingCallbacks.decrement();
     220             :   }
     221             : 
     222             :   /// Run [tearDowns] in reverse order.
     223             :   ///
     224             :   /// An exception thrown in a tearDown callback will cause the test to fail, if
     225             :   /// it isn't already failing, but it won't prevent the remaining callbacks
     226             :   /// from running. This invoker will not be closeable within the zone that the
     227             :   /// teardowns are running in.
     228          11 :   Future<void> runTearDowns(List<FutureOr<void> Function()> tearDowns) {
     229          11 :     heartbeat();
     230          22 :     return runZoned(() async {
     231          11 :       while (tearDowns.isNotEmpty) {
     232           3 :         var completer = Completer();
     233             : 
     234           3 :         addOutstandingCallback();
     235           6 :         _waitForOutstandingCallbacks(() {
     236          12 :           Future.sync(tearDowns.removeLast()).whenComplete(completer.complete);
     237          12 :         }).then((_) => removeOutstandingCallback()).unawaited;
     238             : 
     239           6 :         await completer.future;
     240             :       }
     241          22 :     }, zoneValues: {_forceOpenForTearDownKey: true});
     242             :   }
     243             : 
     244             :   /// Runs [fn] and completes once [fn] and all outstanding callbacks registered
     245             :   /// within [fn] have completed.
     246             :   ///
     247             :   /// Outstanding callbacks registered within [fn] will *not* be registered as
     248             :   /// outstanding callback outside of [fn].
     249          11 :   Future<void> _waitForOutstandingCallbacks(FutureOr<void> Function() fn) {
     250          11 :     heartbeat();
     251             : 
     252             :     Zone? zone;
     253          11 :     var counter = _AsyncCounter();
     254          22 :     runZoned(() async {
     255          11 :       zone = Zone.current;
     256          22 :       _outstandingCallbackZones.add(zone!);
     257          11 :       await fn();
     258          11 :       counter.decrement();
     259          22 :     }, zoneValues: {_counterKey: counter});
     260             : 
     261          33 :     return counter.onZero.whenComplete(() {
     262          22 :       _outstandingCallbackZones.remove(zone!);
     263             :     });
     264             :   }
     265             : 
     266             :   /// Notifies the invoker that progress is being made.
     267             :   ///
     268             :   /// Each heartbeat resets the timeout timer. This helps ensure that
     269             :   /// long-running tests that still make progress don't time out.
     270          11 :   void heartbeat() {
     271          22 :     if (liveTest.isComplete) return;
     272          33 :     if (_timeoutTimer != null) _timeoutTimer!.cancel();
     273             : 
     274             :     const defaultTimeout = Duration(seconds: 30);
     275          55 :     var timeout = liveTest.test.metadata.timeout.apply(defaultTimeout);
     276             :     if (timeout == null) return;
     277           0 :     String message() {
     278           0 :       var message = 'Test timed out after ${niceDuration(timeout)}.';
     279           0 :       if (timeout == defaultTimeout) {
     280           0 :         message += ' See https://pub.dev/packages/test#timeouts';
     281             :       }
     282             :       return message;
     283             :     }
     284             : 
     285          22 :     _timeoutTimer = Zone.root.createTimer(timeout, () {
     286           0 :       _outstandingCallbackZones.last.run(() {
     287           0 :         _handleError(Zone.current, TimeoutException(message(), timeout));
     288             :       });
     289             :     });
     290             :   }
     291             : 
     292             :   /// Marks the current test as skipped.
     293             :   ///
     294             :   /// If passed, [message] is emitted as a skip message.
     295             :   ///
     296             :   /// Note that this *does not* mark the test as complete. That is, it sets
     297             :   /// the result to [Result.skipped], but doesn't change the state.
     298           0 :   void skip([String? message]) {
     299           0 :     if (liveTest.state.shouldBeDone) {
     300             :       // Set the state explicitly so we don't get an extra error about the test
     301             :       // failing after being complete.
     302           0 :       _controller.setState(const State(Status.complete, Result.error));
     303             :       throw 'This test was marked as skipped after it had already completed. '
     304             :           'Make sure to use\n'
     305             :           '[expectAsync] or the [completes] matcher when testing async code.';
     306             :     }
     307             : 
     308           0 :     if (message != null) _controller.message(Message.skip(message));
     309             :     // TODO: error if the test is already complete.
     310           0 :     _controller.setState(const State(Status.pending, Result.skipped));
     311             :   }
     312             : 
     313             :   /// Prints [message] if and when this test fails.
     314           0 :   void printOnFailure(String message) {
     315           0 :     message = message.trim();
     316           0 :     if (liveTest.state.result.isFailing) {
     317           0 :       print('\n$message');
     318             :     } else {
     319           0 :       _printsOnFailure.add(message);
     320             :     }
     321             :   }
     322             : 
     323             :   /// Notifies the invoker of an asynchronous error.
     324             :   ///
     325             :   /// The [zone] is the zone in which the error was thrown.
     326           0 :   void _handleError(Zone zone, Object error, [StackTrace? stackTrace]) {
     327             :     // Ignore errors propagated from previous test runs
     328           0 :     if (_runCount != zone[#runCount]) return;
     329             : 
     330             :     // Get the chain information from the zone in which the error was thrown.
     331           0 :     zone.run(() {
     332             :       if (stackTrace == null) {
     333           0 :         stackTrace = Chain.current();
     334             :       } else {
     335           0 :         stackTrace = Chain.forTrace(stackTrace!);
     336             :       }
     337             :     });
     338             : 
     339             :     // Store these here because they'll change when we set the state below.
     340           0 :     var shouldBeDone = liveTest.state.shouldBeDone;
     341             : 
     342           0 :     if (error is! TestFailure) {
     343           0 :       _controller.setState(const State(Status.complete, Result.error));
     344           0 :     } else if (liveTest.state.result != Result.error) {
     345           0 :       _controller.setState(const State(Status.complete, Result.failure));
     346             :     }
     347             : 
     348           0 :     _controller.addError(error, stackTrace!);
     349           0 :     zone.run(() => _outstandingCallbacks.complete());
     350             : 
     351           0 :     if (_printsOnFailure.isNotEmpty) {
     352           0 :       print(_printsOnFailure.join('\n\n'));
     353           0 :       _printsOnFailure.clear();
     354             :     }
     355             : 
     356             :     // If a test was supposed to be done but then had an error, that indicates
     357             :     // that it was poorly-written and could be flaky.
     358             :     if (!shouldBeDone) return;
     359             : 
     360             :     // However, users don't think of load tests as "tests", so the error isn't
     361             :     // helpful for them.
     362           0 :     if (liveTest.suite.isLoadSuite) return;
     363             : 
     364           0 :     _handleError(
     365             :         zone,
     366             :         'This test failed after it had already completed. Make sure to use '
     367             :         '[expectAsync]\n'
     368             :         'or the [completes] matcher when testing async code.',
     369             :         stackTrace);
     370             :   }
     371             : 
     372             :   /// The method that's run when the test is started.
     373          11 :   void _onRun() {
     374          22 :     _controller.setState(const State(Status.running, Result.success));
     375             : 
     376          22 :     _runCount++;
     377          22 :     Chain.capture(() {
     378          22 :       _guardIfGuarded(() {
     379          22 :         runZoned(() async {
     380             :           // Run the test asynchronously so that the "running" state change
     381             :           // has a chance to hit its event handler(s) before the test produces
     382             :           // an error. If an error is emitted before the first state change is
     383             :           // handled, we can end up with [onError] callbacks firing before the
     384             :           // corresponding [onStateChange], which violates the timing
     385             :           // guarantees.
     386             :           //
     387             :           // Use the event loop over the microtask queue to avoid starvation.
     388          33 :           await Future(() {});
     389             : 
     390          44 :           await _waitForOutstandingCallbacks(_test._body);
     391          55 :           await _waitForOutstandingCallbacks(() => runTearDowns(_tearDowns));
     392             : 
     393          33 :           if (_timeoutTimer != null) _timeoutTimer!.cancel();
     394             : 
     395          44 :           if (liveTest.state.result != Result.success &&
     396           0 :               _runCount < liveTest.test.metadata.retry + 1) {
     397           0 :             _controller.message(Message.print('Retry: ${liveTest.test.name}'));
     398           0 :             _onRun();
     399             :             return;
     400             :           }
     401             : 
     402          66 :           _controller.setState(State(Status.complete, liveTest.state.result));
     403             : 
     404          33 :           _controller.completer.complete();
     405             :         },
     406          11 :             zoneValues: {
     407             :               #test.invoker: this,
     408          11 :               _forceOpenForTearDownKey: false,
     409          11 :               #runCount: _runCount,
     410             :             },
     411             :             zoneSpecification:
     412          11 :                 ZoneSpecification(print: (_, __, ___, line) => _print(line)));
     413             :       });
     414          44 :     }, when: liveTest.test.metadata.chainStackTraces, errorZone: false);
     415             :   }
     416             : 
     417             :   /// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`.
     418          11 :   void _guardIfGuarded(void Function() callback) {
     419          11 :     if (_guarded) {
     420           0 :       Invoker.guard(callback);
     421             :     } else {
     422             :       callback();
     423             :     }
     424             :   }
     425             : 
     426             :   /// Prints [text] as a message to [_controller].
     427           0 :   void _print(String text) => _controller.message(Message.print(text));
     428             : }
     429             : 
     430             : /// A manually incremented/decremented counter that completes a [Future] the
     431             : /// first time it reaches zero or is forcefully completed.
     432             : class _AsyncCounter {
     433             :   var _count = 1;
     434             : 
     435             :   /// A Future that completes the first time the counter reaches 0.
     436          33 :   Future<void> get onZero => _completer.future;
     437             :   final _completer = Completer<void>();
     438             : 
     439           6 :   void increment() {
     440          12 :     _count++;
     441             :   }
     442             : 
     443          11 :   void decrement() {
     444          22 :     _count--;
     445          22 :     if (_count != 0) return;
     446          22 :     if (_completer.isCompleted) return;
     447          22 :     _completer.complete();
     448             :   }
     449             : 
     450             :   /// Force [onZero] to complete.
     451             :   ///
     452             :   /// No effect if [onZero] has already completed.
     453           0 :   void complete() {
     454           0 :     if (!_completer.isCompleted) _completer.complete();
     455             :   }
     456             : }
     457             : 
     458             : extension<T> on Future<T> {
     459           3 :   void get unawaited {}
     460             : }

Generated by: LCOV version 1.14