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