| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011, 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 /** | |
| 6 * Description text of the current test group. If multiple groups are nested, | |
| 7 * this will contain all of their text concatenated. | |
| 8 */ | |
| 9 String _currentGroup = ''; | |
| 10 | |
| 11 /** Tests executed in this suite. */ | |
| 12 List<TestCase> _tests; | |
| 13 | |
| 14 /** Whether this is run within dartium layout tests. */ | |
| 15 bool _isLayoutTest = false; | |
| 16 | |
| 17 /** Current test being executed. */ | |
| 18 int _currentTest = 0; | |
| 19 | |
| 20 /** Total number of callbacks that have been executed in the current test. */ | |
| 21 int _callbacksCalled = 0; | |
| 22 | |
| 23 // TODO(rnystrom): Get rid of this if we get canonical closures for methods. | |
| 24 EventListener _onErrorClosure; | |
| 25 | |
| 26 final _UNINITIALIZED = 0; | |
| 27 final _READY = 1; | |
| 28 final _RUNNING_TEST = 2; | |
| 29 | |
| 30 /** | |
| 31 * Whether an undetected error occurred while running the last test. These | |
| 32 * errors are commonly caused by DOM callbacks that were not guarded in a | |
| 33 * try-catch block. | |
| 34 */ | |
| 35 final _UNCAUGHT_ERROR = 3; | |
| 36 | |
| 37 int _state = _UNINITIALIZED; | |
| 38 | |
| 39 /** Creates an expectation for the given value. */ | |
| 40 Expectation expect(value) => new Expectation(value); | |
| 41 | |
| 42 /** Evaluates the given function and validates that it throws an exception. */ | |
| 43 void expectThrow(function) { | |
| 44 bool threw = false; | |
| 45 try { | |
| 46 function(); | |
| 47 } catch (var e) { | |
| 48 threw = true; | |
| 49 } | |
| 50 Expect.equals(true, threw, 'Expected exception but none was thrown.'); | |
| 51 } | |
| 52 | |
| 53 /** | |
| 54 * Creates a new test case with the given description and body. The | |
| 55 * description will include the descriptions of any surrounding group() | |
| 56 * calls. | |
| 57 */ | |
| 58 void test(String spec, TestFunction body) { | |
| 59 _ensureInitialized(); | |
| 60 | |
| 61 _tests.add(new TestCase(_tests.length + 1, _fullSpec(spec), body, 0)); | |
| 62 } | |
| 63 | |
| 64 /** | |
| 65 * Creates a new async test case with the given description and body. The | |
| 66 * description will include the descriptions of any surrounding group() | |
| 67 * calls. | |
| 68 */ | |
| 69 void asyncTest(String spec, int callbacks, TestFunction body) { | |
| 70 _ensureInitialized(); | |
| 71 | |
| 72 final testCase = new TestCase( | |
| 73 _tests.length + 1, _fullSpec(spec), body, callbacks); | |
| 74 _tests.add(testCase); | |
| 75 | |
| 76 if (callbacks < 1) { | |
| 77 testCase.recordError( | |
| 78 'Async tests must wait for at least one callback ', ''); | |
| 79 } | |
| 80 } | |
| 81 | |
| 82 /** | |
| 83 * Creates a new named group of tests. Calls to group() or test() within the | |
| 84 * body of the function passed to this will inherit this group's description. | |
| 85 */ | |
| 86 void group(String description, void body()) { | |
| 87 _ensureInitialized(); | |
| 88 | |
| 89 // Concatenate the new group. | |
| 90 final oldGroup = _currentGroup; | |
| 91 if (_currentGroup != '') { | |
| 92 // Add a space. | |
| 93 _currentGroup = '$_currentGroup $description'; | |
| 94 } else { | |
| 95 // The first group. | |
| 96 _currentGroup = description; | |
| 97 } | |
| 98 | |
| 99 try { | |
| 100 body(); | |
| 101 } finally { | |
| 102 // Now that the group is over, restore the previous one. | |
| 103 _currentGroup = oldGroup; | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 /** Called by subclasses to indicate that an asynchronous test completed. */ | |
| 108 void callbackDone() { | |
| 109 _callbacksCalled++; | |
| 110 final testCase = _tests[_currentTest]; | |
| 111 if (testCase.callbacks == 0) { | |
| 112 testCase.recordError( | |
| 113 "Can't call callbackDone() on a synchronous test", ''); | |
| 114 _state = _UNCAUGHT_ERROR; | |
| 115 } else if (_callbacksCalled > testCase.callbacks) { | |
| 116 final expected = testCase.callbacks; | |
| 117 testCase.recordError( | |
| 118 'More calls to callbackDone() than expected. ' | |
| 119 + 'Actual: ${_callbacksCalled}, expected: ${expected}', ''); | |
| 120 _state = _UNCAUGHT_ERROR; | |
| 121 } else if ((_callbacksCalled == testCase.callbacks) && | |
| 122 (_state != _RUNNING_TEST)) { | |
| 123 testCase.recordSuccess(); | |
| 124 _currentTest++; | |
| 125 _nextBatch(); | |
| 126 } | |
| 127 } | |
| 128 | |
| 129 void forLayoutTests() { | |
| 130 _isLayoutTest = true; | |
| 131 } | |
| 132 | |
| 133 /** Runs all queued tests, one at a time. */ | |
| 134 _runTests() { | |
| 135 window.postMessage('unittest-suite-start', '*'); | |
| 136 window.setTimeout(() { | |
| 137 assert (_currentTest == 0); | |
| 138 | |
| 139 // Listen for uncaught errors. | |
| 140 window.onerror = _onErrorClosure; | |
| 141 _nextBatch(); | |
| 142 }, 0); | |
| 143 } | |
| 144 | |
| 145 /** Runs a single test. */ | |
| 146 _runTest(TestCase testCase) { | |
| 147 try { | |
| 148 // TODO(sigmund): remove this declaration once dartc supports trapping error | |
| 149 // traces. | |
| 150 var trace = ''; | |
| 151 _callbacksCalled = 0; | |
| 152 _state = _RUNNING_TEST; | |
| 153 | |
| 154 testCase.test(); | |
| 155 | |
| 156 if (_state != _UNCAUGHT_ERROR) { | |
| 157 if (testCase.callbacks == _callbacksCalled) { | |
| 158 testCase.recordSuccess(); | |
| 159 } | |
| 160 } | |
| 161 } catch (ExpectException e, var trace) { | |
| 162 if (_state != _UNCAUGHT_ERROR) { | |
| 163 testCase.recordFail(e.message, trace.toString()); | |
| 164 } | |
| 165 } catch (var e, var trace) { | |
| 166 if (_state != _UNCAUGHT_ERROR) { | |
| 167 testCase.recordError('Caught ${e}', trace.toString()); | |
| 168 } | |
| 169 } finally { | |
| 170 _state = _READY; | |
| 171 } | |
| 172 } | |
| 173 | |
| 174 /** | |
| 175 * Runs a batch of tests, yielding whenever an asynchronous test starts | |
| 176 * running. Tests will resume executing when such asynchronous test calls | |
| 177 * [done] or if it fails with an exception. | |
| 178 */ | |
| 179 _nextBatch() { | |
| 180 while (_currentTest < _tests.length) { | |
| 181 final testCase = _tests[_currentTest]; | |
| 182 | |
| 183 _runTest(testCase); | |
| 184 | |
| 185 if (!testCase.isComplete && testCase.callbacks > 0) return; | |
| 186 | |
| 187 _currentTest++; | |
| 188 } | |
| 189 | |
| 190 _completeTests(); | |
| 191 } | |
| 192 | |
| 193 /** Publish results on the page and notify controller. */ | |
| 194 _completeTests() { | |
| 195 window.onerror = null; | |
| 196 | |
| 197 _state = _UNINITIALIZED; | |
| 198 | |
| 199 int testsFailed = 0; | |
| 200 int testsErrors = 0; | |
| 201 int testsPassed = 0; | |
| 202 | |
| 203 for (TestCase t in _tests) { | |
| 204 if (t.success) testsPassed++; | |
| 205 if (t.fail) testsFailed++; | |
| 206 if (t.error) testsErrors++; | |
| 207 } | |
| 208 | |
| 209 if (_isLayoutTest && testsPassed == _tests.length) { | |
| 210 document.body.innerHTML = "PASS"; | |
| 211 } else { | |
| 212 var newBody = new StringBuffer(); | |
| 213 newBody.add("<table class='unittest-table'><tbody>"); | |
| 214 newBody.add(testsPassed == _tests.length | |
| 215 ? "<tr><td colspan='3' class='unittest-pass'>PASS</td></tr>" | |
| 216 : "<tr><td colspan='3' class='unittest-fail'>FAIL</td></tr>"); | |
| 217 | |
| 218 for (final test in _tests) { | |
| 219 newBody.add(test.message); | |
| 220 } | |
| 221 | |
| 222 if (testsPassed == _tests.length) { | |
| 223 newBody.add("<tr><td colspan='3' class='unittest-pass'>All " | |
| 224 + testsPassed + " tests passed</td></tr>"); | |
| 225 } else { | |
| 226 newBody.add(""" | |
| 227 <tr><td colspan='3'>Total | |
| 228 <span class='unittest-pass'>${testsPassed} passed</span>, | |
| 229 <span class='unittest-fail'>${testsFailed} failed</span> | |
| 230 <span class='unittest-error'>${testsErrors} errors</span> | |
| 231 </td></tr>"""); | |
| 232 } | |
| 233 newBody.add("</tbody></table>"); | |
| 234 document.body.innerHTML = newBody.toString(); | |
| 235 } | |
| 236 | |
| 237 window.postMessage('unittest-suite-done', '*'); | |
| 238 } | |
| 239 | |
| 240 void _onError(e) { | |
| 241 if (_currentTest < _tests.length) { | |
| 242 final testCase = _tests[_currentTest]; | |
| 243 // TODO(vsm): figure out how to expose the stack trace here | |
| 244 // Currently e.message works in dartium, but not in dartc. | |
| 245 testCase.recordError('(DOM callback has errors) Caught ${e}', ''); | |
| 246 _state = _UNCAUGHT_ERROR; | |
| 247 if (testCase.callbacks > 0) { | |
| 248 _currentTest++; | |
| 249 _nextBatch(); | |
| 250 } | |
| 251 } | |
| 252 } | |
| 253 | |
| 254 String _fullSpec(String spec) { | |
| 255 if (spec === null) return '$_currentGroup'; | |
| 256 return _currentGroup != '' ? '$_currentGroup $spec' : spec; | |
| 257 } | |
| 258 | |
| 259 /** | |
| 260 * Lazily initializes the test library if not already initialized. | |
| 261 */ | |
| 262 _ensureInitialized() { | |
| 263 if (_state != _UNINITIALIZED) return; | |
| 264 | |
| 265 _tests = <TestCase>[]; | |
| 266 _onErrorClosure = (e) { _onError(e); }; | |
| 267 | |
| 268 // Immediately queue the suite up. It will run after a timeout (i.e. after | |
| 269 // main() has returned). | |
| 270 listener() { | |
| 271 _currentGroup = ''; | |
| 272 _runTests(); | |
| 273 }; | |
| 274 | |
| 275 window.setTimeout(listener, 0); | |
| 276 | |
| 277 _state = _READY; | |
| 278 } | |
| 279 | |
| 280 /** | |
| 281 * Wraps an value and provides an "==" operator that can be used to verify that | |
| 282 * the value matches a given expectation. | |
| 283 */ | |
| 284 class Expectation { | |
| 285 final _value; | |
| 286 | |
| 287 Expectation(this._value); | |
| 288 | |
| 289 /** Asserts that the value is equivalent to [expected]. */ | |
| 290 void equals(expected) { | |
| 291 Expect.equals(expected, _value); | |
| 292 } | |
| 293 | |
| 294 /** | |
| 295 * Asserts that the difference between [expected] and the value is within | |
| 296 * [tolerance]. If no tolerance is given, it is assumed to be the value 4 | |
| 297 * significant digits smaller than the expected value. | |
| 298 */ | |
| 299 void approxEquals(num expected, | |
| 300 [num tolerance = null, String reason = null]) { | |
| 301 Expect.approxEquals(expected, _value, tolerance: tolerance, reason: reason); | |
| 302 } | |
| 303 | |
| 304 /** Asserts that the value is [null]. */ | |
| 305 void isNull() { | |
| 306 Expect.equals(null, _value); | |
| 307 } | |
| 308 | |
| 309 /** Asserts that the value is not [null]. */ | |
| 310 void isNotNull() { | |
| 311 Expect.notEquals(null, _value); | |
| 312 } | |
| 313 | |
| 314 /** Asserts that the value is [true]. */ | |
| 315 void isTrue() { | |
| 316 Expect.equals(true, _value); | |
| 317 } | |
| 318 | |
| 319 /** Asserts that the value is [false]. */ | |
| 320 void isFalse() { | |
| 321 Expect.equals(false, _value); | |
| 322 } | |
| 323 | |
| 324 /** Asserts that the value has the same elements as [expected]. */ | |
| 325 void equalsCollection(Collection expected) { | |
| 326 Expect.listEquals(expected, _value); | |
| 327 } | |
| 328 | |
| 329 /** | |
| 330 * Checks that every element of [expected] is also in [actual], and that | |
| 331 * every element of [actual] is also in [expected]. | |
| 332 */ | |
| 333 void equalsSet(Iterable expected) { | |
| 334 Expect.setEquals(expected, _value); | |
| 335 } | |
| 336 } | |
| 337 | |
| 338 /** Summarizes information about a single test case. */ | |
| 339 class TestCase { | |
| 340 /** Identifier for this test. */ | |
| 341 final id; | |
| 342 | |
| 343 /** A description of what the test is specifying. */ | |
| 344 final String description; | |
| 345 | |
| 346 /** The body of the test case. */ | |
| 347 final TestFunction test; | |
| 348 | |
| 349 /** Total number of callbacks to wait for before the test completes. */ | |
| 350 int callbacks; | |
| 351 | |
| 352 /** Whether this test case was succesful. */ | |
| 353 bool success; | |
| 354 | |
| 355 /** Whether an Expect call failed in this test. */ | |
| 356 bool fail; | |
| 357 | |
| 358 /** Whether this test case had a runtime error. */ | |
| 359 bool error; | |
| 360 | |
| 361 /** Messages to display at the end of the test run. */ | |
| 362 String message; | |
| 363 | |
| 364 TestCase(this.id, this.description, this.test, this.callbacks) | |
| 365 : success = false, | |
| 366 fail = false, | |
| 367 error = false { | |
| 368 message = '''<tr> | |
| 369 <td>${id}</td> | |
| 370 <td class="unittest-error">NO STATUS</td> | |
| 371 <td>Test did not complete</td> | |
| 372 </tr>'''; | |
| 373 } | |
| 374 | |
| 375 bool get isComplete() => success || fail || error; | |
| 376 | |
| 377 void recordSuccess() { | |
| 378 _setMessage('pass', '', null); | |
| 379 success = true; | |
| 380 } | |
| 381 | |
| 382 void recordError(String msg, String stackTrace) { | |
| 383 _setMessage('error', msg, stackTrace); | |
| 384 error = true; | |
| 385 } | |
| 386 | |
| 387 void recordFail(String msg, String stackTrace) { | |
| 388 _setMessage('fail', msg, stackTrace); | |
| 389 fail = true; | |
| 390 } | |
| 391 | |
| 392 void _setMessage(String type, String msg, String stackTrace) { | |
| 393 message = | |
| 394 ''' | |
| 395 <tr> | |
| 396 <td>${id}</td> | |
| 397 <td class="unittest-$type">${type.toUpperCase()}</td> | |
| 398 <td>Expectation: $description. $msg</td> | |
| 399 </tr> | |
| 400 '''; | |
| 401 | |
| 402 if (stackTrace != null) { | |
| 403 message += | |
| 404 '<tr><td></td><td colspan="2"><pre>${stackTrace}</pre></td></tr>'; | |
| 405 } | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 typedef void TestFunction(); | |
| OLD | NEW |