OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 // TODO(nweiz): Add support for calling [schedule] while the schedule is already | 5 // TODO(nweiz): Add support for calling [schedule] while the schedule is already |
6 // running. | 6 // running. |
7 // TODO(nweiz): Port the non-Pub-specific scheduled test libraries from Pub. | 7 // TODO(nweiz): Port the non-Pub-specific scheduled test libraries from Pub. |
8 library scheduled_test; | 8 library scheduled_test; |
9 | 9 |
10 import 'dart:async'; | 10 import 'dart:async'; |
(...skipping 12 matching lines...) Expand all Loading... |
23 export 'src/scheduled_future_matchers.dart'; | 23 export 'src/scheduled_future_matchers.dart'; |
24 export 'src/task.dart'; | 24 export 'src/task.dart'; |
25 | 25 |
26 /// The [Schedule] for the current test. This is used to add new tasks and | 26 /// The [Schedule] for the current test. This is used to add new tasks and |
27 /// inspect the state of the schedule. | 27 /// inspect the state of the schedule. |
28 /// | 28 /// |
29 /// This is `null` when there's no test currently running. | 29 /// This is `null` when there's no test currently running. |
30 Schedule get currentSchedule => _currentSchedule; | 30 Schedule get currentSchedule => _currentSchedule; |
31 Schedule _currentSchedule; | 31 Schedule _currentSchedule; |
32 | 32 |
33 /// The user-provided setUp function. This is set for each test during | 33 /// The user-provided set-up function for the currently-running test. |
34 /// `unittest.setUp`. | 34 /// |
| 35 /// This is set for each test during `unittest.setUp`. |
35 Function _setUpFn; | 36 Function _setUpFn; |
36 | 37 |
| 38 /// The user-provided tear-down function for the currently-running test. |
| 39 /// |
| 40 /// This is set for each test during `unittest.setUp`. |
| 41 Function _tearDownFn; |
| 42 |
| 43 /// The user-provided set-up function for the current test scope. |
| 44 Function _setUpForGroup; |
| 45 |
| 46 /// The user-provided tear-down function for the current test scope. |
| 47 Function _tearDownForGroup; |
| 48 |
37 /// Creates a new test case with the given description and body. | 49 /// Creates a new test case with the given description and body. |
38 /// | 50 /// |
39 /// This has the same semantics as [unittest.test]. | 51 /// This has the same semantics as [unittest.test]. |
40 /// | 52 /// |
41 /// If [body] returns a [Future], that future will automatically be wrapped with | 53 /// If [body] returns a [Future], that future will automatically be wrapped with |
42 /// [wrapFuture]. | 54 /// [wrapFuture]. |
43 void test(String description, body()) => | 55 void test(String description, body()) => |
44 _test(description, body, unittest.test); | 56 _test(description, body, unittest.test); |
45 | 57 |
46 /// Creates a new test case with the given description and body that will be the | 58 /// Creates a new test case with the given description and body that will be the |
47 /// only test run in this file. | 59 /// only test run in this file. |
48 /// | 60 /// |
49 /// This has the same semantics as [unittest.solo_test]. | 61 /// This has the same semantics as [unittest.solo_test]. |
50 /// | 62 /// |
51 /// If [body] returns a [Future], that future will automatically be wrapped with | 63 /// If [body] returns a [Future], that future will automatically be wrapped with |
52 /// [wrapFuture]. | 64 /// [wrapFuture]. |
53 void solo_test(String description, body()) => | 65 void solo_test(String description, body()) => |
54 _test(description, body, unittest.solo_test); | 66 _test(description, body, unittest.solo_test); |
55 | 67 |
56 void _test(String description, body(), Function testFn) { | 68 void _test(String description, body(), Function testFn) { |
57 maybeWrapFuture(future, description) { | 69 maybeWrapFuture(future, description) { |
58 if (future != null) wrapFuture(future, description); | 70 if (future != null) wrapFuture(future, description); |
59 } | 71 } |
60 | 72 |
61 unittest.ensureInitialized(); | 73 unittest.ensureInitialized(); |
62 _ensureSetUpForTopLevel(); | 74 _initializeForGroup(); |
63 testFn(description, () { | 75 testFn(description, () { |
64 var completer = new Completer(); | 76 var completer = new Completer(); |
65 | 77 |
66 // Capture this in a local variable in case we capture an out-of-band error | 78 // Capture this in a local variable in case we capture an out-of-band error |
67 // after the schedule completes. | 79 // after the schedule completes. |
68 var errorHandler; | 80 var errorHandler; |
69 | 81 |
70 Chain.capture(() { | 82 Chain.capture(() { |
71 _currentSchedule = new Schedule(); | 83 _currentSchedule = new Schedule(); |
72 errorHandler = _currentSchedule.signalError; | 84 errorHandler = _currentSchedule.signalError; |
73 return currentSchedule.run(() { | 85 return currentSchedule.run(() { |
74 if (_setUpFn != null) maybeWrapFuture(_setUpFn(), "set up"); | 86 if (_setUpFn != null) maybeWrapFuture(_setUpFn(), "set up"); |
75 maybeWrapFuture(body(), "test body"); | 87 maybeWrapFuture(body(), "test body"); |
| 88 if (_tearDownFn != null) maybeWrapFuture(_tearDownFn(), "tear down"); |
76 }).catchError((error, stackTrace) { | 89 }).catchError((error, stackTrace) { |
77 if (error is ScheduleError) { | 90 if (error is ScheduleError) { |
78 assert(error.schedule.errors.contains(error)); | 91 assert(error.schedule.errors.contains(error)); |
79 assert(error.schedule == currentSchedule); | 92 assert(error.schedule == currentSchedule); |
80 unittest.registerException(error.schedule.errorString()); | 93 unittest.registerException(error.schedule.errorString()); |
81 } else { | 94 } else { |
82 unittest.registerException(error, new Chain.forTrace(stackTrace)); | 95 unittest.registerException(error, new Chain.forTrace(stackTrace)); |
83 } | 96 } |
84 }).then(completer.complete); | 97 }).then(completer.complete); |
85 }, onError: (error, stackTrace) => errorHandler(error, stackTrace)); | 98 }, onError: (error, stackTrace) => errorHandler(error, stackTrace)); |
86 | 99 |
87 return completer.future; | 100 return completer.future; |
88 }); | 101 }); |
89 } | 102 } |
90 | 103 |
91 /// Whether or not the tests currently being defined are in a group. This is | 104 /// Whether or not the tests currently being defined are in a group. This is |
92 /// only true when defining tests, not when executing them. | 105 /// only true when defining tests, not when executing them. |
93 bool _inGroup = false; | 106 bool _inGroup = false; |
94 | 107 |
95 /// Creates a new named group of tests. This has the same semantics as | 108 /// Creates a new named group of tests. This has the same semantics as |
96 /// [unittest.group]. | 109 /// [unittest.group]. |
97 void group(String description, void body()) { | 110 void group(String description, void body()) { |
98 unittest.ensureInitialized(); | 111 unittest.ensureInitialized(); |
99 _ensureSetUpForTopLevel(); | 112 _initializeForGroup(); |
100 unittest.group(description, () { | 113 unittest.group(description, () { |
| 114 var oldSetUp = _setUpForGroup; |
| 115 var oldTearDown = _tearDownForGroup; |
| 116 var wasInitializedForGroup = _initializedForGroup; |
101 var wasInGroup = _inGroup; | 117 var wasInGroup = _inGroup; |
| 118 _setUpForGroup = null; |
| 119 _tearDownForGroup = null; |
| 120 _initializedForGroup = false; |
102 _inGroup = true; | 121 _inGroup = true; |
103 body(); | 122 body(); |
| 123 _setUpForGroup = oldSetUp; |
| 124 _tearDownForGroup = oldTearDown; |
| 125 _initializedForGroup = wasInitializedForGroup; |
104 _inGroup = wasInGroup; | 126 _inGroup = wasInGroup; |
105 }); | 127 }); |
106 } | 128 } |
107 | 129 |
108 /// Schedules a task, [fn], to run asynchronously as part of the main task queue | 130 /// Schedules a task, [fn], to run asynchronously as part of the main task queue |
109 /// of [currentSchedule]. Tasks will be run in the order they're scheduled. If | 131 /// of [currentSchedule]. Tasks will be run in the order they're scheduled. If |
110 /// [fn] returns a [Future], tasks after it won't be run until that [Future] | 132 /// [fn] returns a [Future], tasks after it won't be run until that [Future] |
111 /// completes. | 133 /// completes. |
112 /// | 134 /// |
113 /// The return value will be completed once the scheduled task has finished | 135 /// The return value will be completed once the scheduled task has finished |
114 /// running. Its return value is the same as the return value of [fn], or the | 136 /// running. Its return value is the same as the return value of [fn], or the |
115 /// value it completes to if it's a [Future]. | 137 /// value it completes to if it's a [Future]. |
116 /// | 138 /// |
117 /// If [description] is passed, it's used to describe the task for debugging | 139 /// If [description] is passed, it's used to describe the task for debugging |
118 /// purposes when an error occurs. | 140 /// purposes when an error occurs. |
119 /// | 141 /// |
120 /// If this is called when a task queue is currently running, it will run [fn] | 142 /// If this is called when a task queue is currently running, it will run [fn] |
121 /// on the next event loop iteration rather than adding it to a queue. The | 143 /// on the next event loop iteration rather than adding it to a queue. The |
122 /// current task will not complete until [fn] (and any [Future] it returns) has | 144 /// current task will not complete until [fn] (and any [Future] it returns) has |
123 /// finished running. Any errors in [fn] will automatically be handled. | 145 /// finished running. Any errors in [fn] will automatically be handled. |
124 Future schedule(fn(), [String description]) => | 146 Future schedule(fn(), [String description]) => |
125 currentSchedule.tasks.schedule(fn, description); | 147 currentSchedule.tasks.schedule(fn, description); |
126 | 148 |
127 /// Register a [setUp] function for a test [group]. This has the same semantics | 149 /// Register a [setUp] function for a test [group]. |
128 /// as [unittest.setUp]. Tasks may be scheduled using [schedule] within | |
129 /// [setUpFn], and [currentSchedule] may be accessed as well. | |
130 /// | 150 /// |
131 /// Note that there is no associated [tearDown] function. Instead, tasks should | 151 /// This has the same semantics as [unittest.setUp]. Tasks may be scheduled |
132 /// be scheduled for [currentSchedule.onComplete] or | 152 /// using [schedule] within [setUpFn], and [currentSchedule] may be accessed as |
133 /// [currentSchedule.onException]. These tasks will be run after each test's | 153 /// well. |
134 /// schedule is completed. | |
135 void setUp(setUpFn()) { | 154 void setUp(setUpFn()) { |
136 _setUpScheduledTest(setUpFn); | 155 _setUpForGroup = setUpFn; |
137 } | 156 } |
138 | 157 |
139 /// Whether [unittest.setUp] has been called in the top level scope. | 158 /// Register a [tearDown] function for a test [group]. |
140 bool _setUpForTopLevel = false; | 159 /// |
141 | 160 /// This has the same semantics as [unittest.tearDown]. Tasks may be scheduled |
142 /// If we're in the top-level scope (that is, not in any [group]s) and | 161 /// using [schedule] within [tearDownFn], and [currentSchedule] may be accessed |
143 /// [unittest.setUp] hasn't been called yet, call it. | 162 /// as well. Note that [tearDownFn] will be run synchronously after the test |
144 void _ensureSetUpForTopLevel() { | 163 /// body finishes running, which means it will run before any scheduled tasks |
145 if (_inGroup || _setUpForTopLevel) return; | 164 /// have begun. |
146 _setUpScheduledTest(); | 165 /// |
| 166 /// To run code after the schedule has finished running, use |
| 167 /// `currentSchedule.onComplete.schedule`. |
| 168 void tearDown(tearDownFn()) { |
| 169 _tearDownForGroup = tearDownFn; |
147 } | 170 } |
148 | 171 |
| 172 /// Whether [_initializeForGroup] has been called in this group scope. |
| 173 bool _initializedForGroup = false; |
| 174 |
149 /// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up | 175 /// Registers callbacks for [unittest.setUp] and [unittest.tearDown] that set up |
150 /// and tear down the scheduled test infrastructure. | 176 /// and tear down the scheduled test infrastructure and run the user's [setUp] |
151 void _setUpScheduledTest([void setUpFn()]) { | 177 /// and [tearDown] callbacks. |
152 if (!_inGroup) { | 178 void _initializeForGroup() { |
153 _setUpForTopLevel = true; | 179 if (_initializedForGroup) return; |
154 var oldWrapAsync = unittest.wrapAsync; | 180 _initializedForGroup = true; |
155 unittest.setUp(() { | |
156 if (currentSchedule != null) { | |
157 throw new StateError('There seems to be another scheduled test ' | |
158 'still running.'); | |
159 } | |
160 | 181 |
161 unittest.wrapAsync = (f, [description]) { | 182 var setUpFn = _setUpForGroup; |
162 // It's possible that this setup is run before a vanilla unittest test | 183 var tearDownFn = _tearDownForGroup; |
163 // if [unittest.test] is run in the same context as | |
164 // [scheduled_test.test]. In that case, [currentSchedule] will never be | |
165 // set and we should forward to the [unittest.wrapAsync]. | |
166 if (currentSchedule == null) return oldWrapAsync(f, description); | |
167 return currentSchedule.wrapAsync(f, description); | |
168 }; | |
169 | 184 |
170 if (_setUpFn != null) { | 185 if (_inGroup) { |
171 var parentFn = _setUpFn; | 186 unittest.setUp(() => _addSetUpTearDown(setUpFn, tearDownFn)); |
172 _setUpFn = () { parentFn(); setUpFn(); }; | 187 return; |
173 } else { | 188 } |
174 _setUpFn = setUpFn; | |
175 } | |
176 }); | |
177 | 189 |
178 unittest.tearDown(() { | 190 var oldWrapAsync = unittest.wrapAsync; |
179 unittest.wrapAsync = oldWrapAsync; | 191 unittest.setUp(() { |
180 _currentSchedule = null; | 192 if (currentSchedule != null) { |
181 _setUpFn = null; | 193 throw new StateError('There seems to be another scheduled test ' |
182 }); | 194 'still running.'); |
183 } else { | 195 } |
184 unittest.setUp(() { | 196 |
185 if (_setUpFn != null) { | 197 unittest.wrapAsync = (f, [description]) { |
186 var parentFn = _setUpFn; | 198 // It's possible that this setup is run before a vanilla unittest test |
187 _setUpFn = () { parentFn(); setUpFn(); }; | 199 // if [unittest.test] is run in the same context as |
188 } else { | 200 // [scheduled_test.test]. In that case, [currentSchedule] will never be |
189 _setUpFn = setUpFn; | 201 // set and we should forward to the [unittest.wrapAsync]. |
190 } | 202 if (currentSchedule == null) return oldWrapAsync(f, description); |
191 }); | 203 return currentSchedule.wrapAsync(f, description); |
| 204 }; |
| 205 |
| 206 _addSetUpTearDown(setUpFn, tearDownFn); |
| 207 }); |
| 208 |
| 209 unittest.tearDown(() { |
| 210 unittest.wrapAsync = oldWrapAsync; |
| 211 _currentSchedule = null; |
| 212 _setUpFn = null; |
| 213 _tearDownFn = null; |
| 214 }); |
| 215 } |
| 216 |
| 217 /// Set [_setUpFn] and [_tearDownFn] appropriately. |
| 218 void _addSetUpTearDown(void setUpFn(), void tearDownFn()) { |
| 219 if (setUpFn != null) { |
| 220 if (_setUpFn != null) { |
| 221 var parentFn = _setUpFn; |
| 222 _setUpFn = () { parentFn(); setUpFn(); }; |
| 223 } else { |
| 224 _setUpFn = setUpFn; |
| 225 } |
| 226 } |
| 227 |
| 228 if (tearDownFn != null) { |
| 229 if (_tearDownFn != null) { |
| 230 var parentFn = _tearDownFn; |
| 231 _tearDownFn = () { parentFn(); tearDownFn(); }; |
| 232 } else { |
| 233 _tearDownFn = tearDownFn; |
| 234 } |
192 } | 235 } |
193 } | 236 } |
194 | 237 |
195 /// Like [wrapAsync], this ensures that the current task queue waits for | 238 /// Like [wrapAsync], this ensures that the current task queue waits for |
196 /// out-of-band asynchronous code, and that errors raised in that code are | 239 /// out-of-band asynchronous code, and that errors raised in that code are |
197 /// handled correctly. However, [wrapFuture] wraps a [Future] chain rather than | 240 /// handled correctly. However, [wrapFuture] wraps a [Future] chain rather than |
198 /// a single callback. | 241 /// a single callback. |
199 /// | 242 /// |
200 /// The returned [Future] completes to the same value or error as [future]. | 243 /// The returned [Future] completes to the same value or error as [future]. |
201 /// | 244 /// |
202 /// [description] provides an optional description of the future, which is | 245 /// [description] provides an optional description of the future, which is |
203 /// used when generating error messages. | 246 /// used when generating error messages. |
204 Future wrapFuture(Future future, [String description]) { | 247 Future wrapFuture(Future future, [String description]) { |
205 if (currentSchedule == null) { | 248 if (currentSchedule == null) { |
206 throw new StateError("Unexpected call to wrapFuture with no current " | 249 throw new StateError("Unexpected call to wrapFuture with no current " |
207 "schedule."); | 250 "schedule."); |
208 } | 251 } |
209 | 252 |
210 return currentSchedule.wrapFuture(future, description); | 253 return currentSchedule.wrapFuture(future, description); |
211 } | 254 } |
OLD | NEW |