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.frontend.expect_async; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import '../backend/invoker.dart'; |
| 10 import '../backend/state.dart'; |
| 11 import 'expect.dart'; |
| 12 |
| 13 /// An object used to detect unpassed arguments. |
| 14 const _PLACEHOLDER = const Object(); |
| 15 |
| 16 // Functions used to check how many arguments a callback takes. |
| 17 typedef _Func0(); |
| 18 typedef _Func1(a); |
| 19 typedef _Func2(a, b); |
| 20 typedef _Func3(a, b, c); |
| 21 typedef _Func4(a, b, c, d); |
| 22 typedef _Func5(a, b, c, d, e); |
| 23 typedef _Func6(a, b, c, d, e, f); |
| 24 |
| 25 typedef bool _IsDoneCallback(); |
| 26 |
| 27 /// A wrapper for a function that ensures that it's called the appropriate |
| 28 /// number of times. |
| 29 /// |
| 30 /// The containing test won't be considered to have completed successfully until |
| 31 /// this function has been called the appropriate number of times. |
| 32 /// |
| 33 /// The wrapper function is accessible via [func]. It supports up to six |
| 34 /// optional and/or required positional arguments, but no named arguments. |
| 35 class _ExpectedFunction { |
| 36 /// The wrapped callback. |
| 37 final Function _callback; |
| 38 |
| 39 /// The minimum number of calls that are expected to be made to the function. |
| 40 /// |
| 41 /// If fewer calls than this are made, the test will fail. |
| 42 final int _minExpectedCalls; |
| 43 |
| 44 /// The maximum number of calls that are expected to be made to the function. |
| 45 /// |
| 46 /// If more calls than this are made, the test will fail. |
| 47 final int _maxExpectedCalls; |
| 48 |
| 49 /// A callback that should return whether the function is not expected to have |
| 50 /// any more calls. |
| 51 /// |
| 52 /// This will be called after every time the function is run. The test case |
| 53 /// won't be allowed to terminate until it returns `true`. |
| 54 /// |
| 55 /// This may be `null`. If so, the function is considered to be done after |
| 56 /// it's been run once. |
| 57 final _IsDoneCallback _isDone; |
| 58 |
| 59 /// A descriptive name for the function. |
| 60 final String _id; |
| 61 |
| 62 /// An optional description of why the function is expected to be called. |
| 63 /// |
| 64 /// If not passed, this will be an empty string. |
| 65 final String _reason; |
| 66 |
| 67 /// The number of times the function has been called. |
| 68 int _actualCalls = 0; |
| 69 |
| 70 /// The test invoker in which this function was wrapped. |
| 71 Invoker get _invoker => _zone[#test.invoker]; |
| 72 |
| 73 /// The zone in which this function was wrapped. |
| 74 final Zone _zone; |
| 75 |
| 76 /// Whether this function has been called the requisite number of times. |
| 77 bool _complete; |
| 78 |
| 79 /// Wraps [callback] in a function that asserts that it's called at least |
| 80 /// [minExpected] times and no more than [maxExpected] times. |
| 81 /// |
| 82 /// If passed, [id] is used as a descriptive name fo the function and [reason] |
| 83 /// as a reason it's expected to be called. If [isDone] is passed, the test |
| 84 /// won't be allowed to complete until it returns `true`. |
| 85 _ExpectedFunction(Function callback, int minExpected, int maxExpected, |
| 86 {String id, String reason, bool isDone()}) |
| 87 : this._callback = callback, |
| 88 _minExpectedCalls = minExpected, |
| 89 _maxExpectedCalls = (maxExpected == 0 && minExpected > 0) |
| 90 ? minExpected |
| 91 : maxExpected, |
| 92 this._isDone = isDone, |
| 93 this._reason = reason == null ? '' : '\n$reason', |
| 94 this._zone = Zone.current, |
| 95 this._id = _makeCallbackId(id, callback) { |
| 96 if (_invoker == null) { |
| 97 throw new StateError("[expectAsync] was called outside of a test."); |
| 98 } else if (maxExpected > 0 && minExpected > maxExpected) { |
| 99 throw new ArgumentError("max ($maxExpected) may not be less than count " |
| 100 "($minExpected)."); |
| 101 } |
| 102 |
| 103 if (isDone != null || minExpected > 0) { |
| 104 _invoker.addOutstandingCallback(); |
| 105 _complete = false; |
| 106 } else { |
| 107 _complete = true; |
| 108 } |
| 109 } |
| 110 |
| 111 /// Tries to find a reasonable name for [callback]. |
| 112 /// |
| 113 /// If [id] is passed, uses that. Otherwise, tries to determine a name from |
| 114 /// calling `toString`. If no name can be found, returns the empty string. |
| 115 static String _makeCallbackId(String id, Function callback) { |
| 116 if (id != null) return "$id "; |
| 117 |
| 118 // If the callback is not an anonymous closure, try to get the |
| 119 // name. |
| 120 var toString = callback.toString(); |
| 121 var prefix = "Function '"; |
| 122 var start = toString.indexOf(prefix); |
| 123 if (start == -1) return ''; |
| 124 |
| 125 start += prefix.length; |
| 126 var end = toString.indexOf("'", start); |
| 127 if (end == -1) return ''; |
| 128 return "${toString.substring(start, end)} "; |
| 129 } |
| 130 |
| 131 /// Returns a function that has the same number of positional arguments as the |
| 132 /// wrapped function (up to a total of 6). |
| 133 Function get func { |
| 134 if (_callback is _Func6) return _max6; |
| 135 if (_callback is _Func5) return _max5; |
| 136 if (_callback is _Func4) return _max4; |
| 137 if (_callback is _Func3) return _max3; |
| 138 if (_callback is _Func2) return _max2; |
| 139 if (_callback is _Func1) return _max1; |
| 140 if (_callback is _Func0) return _max0; |
| 141 |
| 142 _invoker.removeOutstandingCallback(); |
| 143 throw new ArgumentError( |
| 144 'The wrapped function has more than 6 required arguments'); |
| 145 } |
| 146 |
| 147 // This indirection is critical. It ensures the returned function has an |
| 148 // argument count of zero. |
| 149 _max0() => _max6(); |
| 150 |
| 151 _max1([a0 = _PLACEHOLDER]) => _max6(a0); |
| 152 |
| 153 _max2([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER]) => _max6(a0, a1); |
| 154 |
| 155 _max3([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER]) => |
| 156 _max6(a0, a1, a2); |
| 157 |
| 158 _max4([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, |
| 159 a3 = _PLACEHOLDER]) => _max6(a0, a1, a2, a3); |
| 160 |
| 161 _max5([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, |
| 162 a3 = _PLACEHOLDER, a4 = _PLACEHOLDER]) => _max6(a0, a1, a2, a3, a4); |
| 163 |
| 164 _max6([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, |
| 165 a3 = _PLACEHOLDER, a4 = _PLACEHOLDER, a5 = _PLACEHOLDER]) => |
| 166 _run([a0, a1, a2, a3, a4, a5].where((a) => a != _PLACEHOLDER)); |
| 167 |
| 168 /// Runs the wrapped function with [args] and returns its return value. |
| 169 _run(Iterable args) { |
| 170 // Note that in the old test, this returned `null` if it encountered an |
| 171 // error, where now it just re-throws that error because Zone machinery will |
| 172 // pass it to the invoker anyway. |
| 173 try { |
| 174 _actualCalls++; |
| 175 if (_invoker.liveTest.isComplete && |
| 176 _invoker.liveTest.state.result == Result.success) { |
| 177 throw 'Callback ${_id}called ($_actualCalls) after test case ' |
| 178 '${_invoker.liveTest.test.name} had already completed.$_reason'; |
| 179 } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) { |
| 180 throw new TestFailure('Callback ${_id}called more times than expected ' |
| 181 '($_maxExpectedCalls).$_reason'); |
| 182 } |
| 183 |
| 184 return Function.apply(_callback, args.toList()); |
| 185 } catch (error, stackTrace) { |
| 186 _zone.handleUncaughtError(error, stackTrace); |
| 187 return null; |
| 188 } finally { |
| 189 _afterRun(); |
| 190 } |
| 191 } |
| 192 |
| 193 /// After each time the function is run, check to see if it's complete. |
| 194 void _afterRun() { |
| 195 if (_complete) return; |
| 196 if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return; |
| 197 if (_isDone != null && !_isDone()) return; |
| 198 |
| 199 // Mark this callback as complete and remove it from the test case's |
| 200 // oustanding callback count; if that hits zero the test is done. |
| 201 _complete = true; |
| 202 _invoker.removeOutstandingCallback(); |
| 203 } |
| 204 } |
| 205 |
| 206 /// Indicate that [callback] is expected to be called [count] number of times |
| 207 /// (by default 1). |
| 208 /// |
| 209 /// The test framework will wait for the callback to run the [count] times |
| 210 /// before it considers the current test to be complete. [callback] may take up |
| 211 /// to six optional or required positional arguments; named arguments are not |
| 212 /// supported. |
| 213 /// |
| 214 /// [max] can be used to specify an upper bound on the number of calls; if this |
| 215 /// is exceeded the test will fail. If [max] is `0` (the default), the callback |
| 216 /// is expected to be called exactly [count] times. If [max] is `-1`, the |
| 217 /// callback is allowed to be called any number of times greater than [count]. |
| 218 /// |
| 219 /// Both [id] and [reason] are optional and provide extra information about the |
| 220 /// callback when debugging. [id] should be the name of the callback, while |
| 221 /// [reason] should be the reason the callback is expected to be called. |
| 222 Function expectAsync(Function callback, |
| 223 {int count: 1, int max: 0, String id, String reason}) { |
| 224 if (Invoker.current == null) { |
| 225 throw new StateError("expectAsync() may only be called within a test."); |
| 226 } |
| 227 |
| 228 return new _ExpectedFunction(callback, count, max, id: id, reason: reason) |
| 229 .func; |
| 230 } |
| 231 |
| 232 /// Indicate that [callback] is expected to be called until [isDone] returns |
| 233 /// true. |
| 234 /// |
| 235 /// [isDone] is called after each time the function is run. Only when it returns |
| 236 /// true will the callback be considered complete. [callback] may take up to six |
| 237 /// optional or required positional arguments; named arguments are not |
| 238 /// supported. |
| 239 /// |
| 240 /// Both [id] and [reason] are optional and provide extra information about the |
| 241 /// callback when debugging. [id] should be the name of the callback, while |
| 242 /// [reason] should be the reason the callback is expected to be called. |
| 243 Function expectAsyncUntil(Function callback, bool isDone(), |
| 244 {String id, String reason}) { |
| 245 if (Invoker.current == null) { |
| 246 throw new StateError( |
| 247 "expectAsyncUntil() may only be called within a test."); |
| 248 } |
| 249 |
| 250 return new _ExpectedFunction(callback, 0, -1, |
| 251 id: id, reason: reason, isDone: isDone).func; |
| 252 } |
OLD | NEW |