OLD | NEW |
(Empty) | |
| 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 library test.backend.invoker; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import 'package:stack_trace/stack_trace.dart'; |
| 10 |
| 11 import '../frontend/expect.dart'; |
| 12 import '../utils.dart'; |
| 13 import 'closed_exception.dart'; |
| 14 import 'live_test.dart'; |
| 15 import 'live_test_controller.dart'; |
| 16 import 'metadata.dart'; |
| 17 import 'outstanding_callback_counter.dart'; |
| 18 import 'state.dart'; |
| 19 import 'suite.dart'; |
| 20 import 'test.dart'; |
| 21 |
| 22 /// A test in this isolate. |
| 23 class LocalTest implements Test { |
| 24 final String name; |
| 25 final Metadata metadata; |
| 26 |
| 27 /// The test body. |
| 28 final AsyncFunction _body; |
| 29 |
| 30 /// The callback used to clean up after the test. |
| 31 /// |
| 32 /// This is separated out from [_body] because it needs to run once the test's |
| 33 /// asynchronous computation has finished, even if that's different from the |
| 34 /// completion of the main body of the test. |
| 35 final AsyncFunction _tearDown; |
| 36 |
| 37 LocalTest(this.name, this.metadata, body(), {tearDown()}) |
| 38 : _body = body, |
| 39 _tearDown = tearDown; |
| 40 |
| 41 /// Loads a single runnable instance of this test. |
| 42 LiveTest load(Suite suite) { |
| 43 var invoker = new Invoker._(suite, this); |
| 44 return invoker.liveTest; |
| 45 } |
| 46 |
| 47 Test change({String name, Metadata metadata}) { |
| 48 if (name == name && metadata == this.metadata) return this; |
| 49 if (name == null) name = this.name; |
| 50 if (metadata == null) metadata = this.metadata; |
| 51 return new LocalTest(name, metadata, _body, tearDown: _tearDown); |
| 52 } |
| 53 } |
| 54 |
| 55 /// The class responsible for managing the lifecycle of a single local test. |
| 56 /// |
| 57 /// The current invoker is accessible within the zone scope of the running test |
| 58 /// using [Invoker.current]. It's used to track asynchronous callbacks and |
| 59 /// report asynchronous errors. |
| 60 class Invoker { |
| 61 /// The live test being driven by the invoker. |
| 62 /// |
| 63 /// This provides a view into the state of the test being executed. |
| 64 LiveTest get liveTest => _controller.liveTest; |
| 65 LiveTestController _controller; |
| 66 |
| 67 /// Whether the test has been closed. |
| 68 /// |
| 69 /// Once the test is closed, [expect] and [expectAsync] will throw |
| 70 /// [ClosedException]s whenever accessed to help the test stop executing as |
| 71 /// soon as possible. |
| 72 bool get closed => _onCloseCompleter.isCompleted; |
| 73 |
| 74 /// A future that completes once the test has been closed. |
| 75 Future get onClose => _onCloseCompleter.future; |
| 76 final _onCloseCompleter = new Completer(); |
| 77 |
| 78 /// The test being run. |
| 79 LocalTest get _test => liveTest.test as LocalTest; |
| 80 |
| 81 /// The test metadata merged with the suite metadata. |
| 82 final Metadata metadata; |
| 83 |
| 84 /// The outstanding callback counter for the current zone. |
| 85 OutstandingCallbackCounter get _outstandingCallbacks { |
| 86 var counter = Zone.current[this]; |
| 87 if (counter != null) return counter; |
| 88 throw new StateError("Can't add or remove outstanding callbacks outside " |
| 89 "of a test body."); |
| 90 } |
| 91 |
| 92 /// The current invoker, or `null` if none is defined. |
| 93 /// |
| 94 /// An invoker is only set within the zone scope of a running test. |
| 95 static Invoker get current { |
| 96 // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526). |
| 97 return Zone.current[#test.invoker]; |
| 98 } |
| 99 |
| 100 /// The zone that the top level of [_test.body] is running in. |
| 101 /// |
| 102 /// Tracking this ensures that [_timeoutTimer] isn't created in a |
| 103 /// timer-mocking zone created by the test. |
| 104 Zone _invokerZone; |
| 105 |
| 106 /// The timer for tracking timeouts. |
| 107 /// |
| 108 /// This will be `null` until the test starts running. |
| 109 Timer _timeoutTimer; |
| 110 |
| 111 Invoker._(Suite suite, LocalTest test) |
| 112 : metadata = suite.metadata.merge(test.metadata) { |
| 113 _controller = new LiveTestController( |
| 114 suite, test, _onRun, _onCloseCompleter.complete); |
| 115 } |
| 116 |
| 117 /// Tells the invoker that there's a callback running that it should wait for |
| 118 /// before considering the test successful. |
| 119 /// |
| 120 /// Each call to [addOutstandingCallback] should be followed by a call to |
| 121 /// [removeOutstandingCallback] once the callbak is no longer running. Note |
| 122 /// that only successful tests wait for outstanding callbacks; as soon as a |
| 123 /// test experiences an error, any further calls to [addOutstandingCallback] |
| 124 /// or [removeOutstandingCallback] will do nothing. |
| 125 /// |
| 126 /// Throws a [ClosedException] if this test has been closed. |
| 127 void addOutstandingCallback() { |
| 128 if (closed) throw new ClosedException(); |
| 129 _outstandingCallbacks.addOutstandingCallback(); |
| 130 } |
| 131 |
| 132 /// Tells the invoker that a callback declared with [addOutstandingCallback] |
| 133 /// is no longer running. |
| 134 void removeOutstandingCallback() { |
| 135 heartbeat(); |
| 136 _outstandingCallbacks.removeOutstandingCallback(); |
| 137 } |
| 138 |
| 139 /// Removes all outstanding callbacks, for example when an error occurs. |
| 140 /// |
| 141 /// Future calls to [addOutstandingCallback] and [removeOutstandingCallback] |
| 142 /// will be ignored. |
| 143 void removeAllOutstandingCallbacks() => |
| 144 _outstandingCallbacks.removeAllOutstandingCallbacks(); |
| 145 |
| 146 /// Runs [fn] and returns once all (registered) outstanding callbacks it |
| 147 /// transitively invokes have completed. |
| 148 /// |
| 149 /// If [fn] itself returns a future, this will automatically wait until that |
| 150 /// future completes as well. |
| 151 /// |
| 152 /// Note that outstanding callbacks registered within [fn] will *not* be |
| 153 /// registered as outstanding callback outside of [fn]. |
| 154 Future waitForOutstandingCallbacks(fn()) { |
| 155 heartbeat(); |
| 156 |
| 157 var counter = new OutstandingCallbackCounter(); |
| 158 runZoned(() { |
| 159 // TODO(nweiz): Use async/await here once issue 23497 has been fixed in |
| 160 // two stable versions. |
| 161 new Future.sync(fn).then((_) => counter.removeOutstandingCallback()); |
| 162 }, zoneValues: { |
| 163 // Use the invoker as a key so that multiple invokers can have different |
| 164 // outstanding callback counters at once. |
| 165 this: counter |
| 166 }); |
| 167 |
| 168 return counter.noOutstandingCallbacks; |
| 169 } |
| 170 |
| 171 /// Notifies the invoker that progress is being made. |
| 172 /// |
| 173 /// Each heartbeat resets the timeout timer. This helps ensure that |
| 174 /// long-running tests that still make progress don't time out. |
| 175 void heartbeat() { |
| 176 if (liveTest.isComplete) return; |
| 177 if (_timeoutTimer != null) _timeoutTimer.cancel(); |
| 178 |
| 179 var timeout = metadata.timeout.apply(new Duration(seconds: 30)); |
| 180 if (timeout == null) return; |
| 181 _timeoutTimer = _invokerZone.createTimer(timeout, |
| 182 Zone.current.bindCallback(() { |
| 183 if (liveTest.isComplete) return; |
| 184 _handleError( |
| 185 new TimeoutException( |
| 186 "Test timed out after ${niceDuration(timeout)}.", timeout)); |
| 187 })); |
| 188 } |
| 189 |
| 190 /// Notifies the invoker of an asynchronous error. |
| 191 void _handleError(error, [StackTrace stackTrace]) { |
| 192 if (stackTrace == null) stackTrace = new Chain.current(); |
| 193 |
| 194 var afterSuccess = liveTest.isComplete && |
| 195 liveTest.state.result == Result.success; |
| 196 |
| 197 if (error is! TestFailure) { |
| 198 _controller.setState(const State(Status.complete, Result.error)); |
| 199 } else if (liveTest.state.result != Result.error) { |
| 200 _controller.setState(const State(Status.complete, Result.failure)); |
| 201 } |
| 202 |
| 203 _controller.addError(error, stackTrace); |
| 204 removeAllOutstandingCallbacks(); |
| 205 |
| 206 // If a test was marked as success but then had an error, that indicates |
| 207 // that it was poorly-written and could be flaky. |
| 208 if (!afterSuccess) return; |
| 209 _handleError( |
| 210 "This test failed after it had already completed. Make sure to use " |
| 211 "[expectAsync]\n" |
| 212 "or the [completes] matcher when testing async code.", |
| 213 stackTrace); |
| 214 } |
| 215 |
| 216 /// The method that's run when the test is started. |
| 217 void _onRun() { |
| 218 _controller.setState(const State(Status.running, Result.success)); |
| 219 |
| 220 var outstandingCallbacksForBody = new OutstandingCallbackCounter(); |
| 221 |
| 222 // TODO(nweiz): Use async/await here once issue 23497 has been fixed in two |
| 223 // stable versions. |
| 224 Chain.capture(() { |
| 225 runZonedWithValues(() { |
| 226 _invokerZone = Zone.current; |
| 227 |
| 228 heartbeat(); |
| 229 |
| 230 // Run the test asynchronously so that the "running" state change has |
| 231 // a chance to hit its event handler(s) before the test produces an |
| 232 // error. If an error is emitted before the first state change is |
| 233 // handled, we can end up with [onError] callbacks firing before the |
| 234 // corresponding [onStateChange], which violates the timing |
| 235 // guarantees. |
| 236 new Future(_test._body) |
| 237 .then((_) => removeOutstandingCallback()); |
| 238 |
| 239 _outstandingCallbacks.noOutstandingCallbacks.then((_) { |
| 240 if (_test._tearDown == null) return null; |
| 241 |
| 242 // Reset the outstanding callback counter to wait for callbacks from |
| 243 // the test's `tearDown` to complete. |
| 244 return waitForOutstandingCallbacks(() => |
| 245 runZoned(_test._tearDown, onError: _handleError)); |
| 246 }).then((_) { |
| 247 if (_timeoutTimer != null) _timeoutTimer.cancel(); |
| 248 _controller.setState( |
| 249 new State(Status.complete, liveTest.state.result)); |
| 250 |
| 251 // Use [Timer.run] here to avoid starving the DOM or other |
| 252 // non-microtask events. |
| 253 Timer.run(_controller.completer.complete); |
| 254 }); |
| 255 }, zoneValues: { |
| 256 #test.invoker: this, |
| 257 // Use the invoker as a key so that multiple invokers can have different |
| 258 // outstanding callback counters at once. |
| 259 this: outstandingCallbacksForBody |
| 260 }, |
| 261 zoneSpecification: new ZoneSpecification( |
| 262 print: (self, parent, zone, line) => _controller.print(line)), |
| 263 onError: _handleError); |
| 264 }); |
| 265 } |
| 266 } |
OLD | NEW |