Index: lib/unittest/unittest.dart |
diff --git a/lib/unittest/unittest.dart b/lib/unittest/unittest.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..26ebddaad162e3d155b723e654b9ebf709f8cdda |
--- /dev/null |
+++ b/lib/unittest/unittest.dart |
@@ -0,0 +1,450 @@ |
+// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/** |
+ * A library for writing dart unit tests. |
+ * |
+ * ##Concepts## |
+ * |
+ * * Tests: Tests are specified via the top-level function [test], they can be |
+ * organized together using [group]. |
+ * * Checks: Test expectations can be specified via [expect] (see methods in |
+ * [Expectation]), [expectThrows], or using assertions with the [Expect] |
+ * class. |
+ * * Configuration: The framework can be adapted by calling [configure] with a |
+ * configuration. Common configurations can be found in this package under: |
+ * 'dom\_config.dart', 'html\_config.dart', and 'vm\_config.dart'. |
+ * |
+ * ##Examples## |
+ * |
+ * A trivial test: |
+ * |
+ * #import('path-to-dart/lib/unittest/unitest.dart'); |
+ * main() { |
+ * test('this is a test', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * } |
+ * |
+ * Multiple tests: |
+ * |
+ * #import('path-to-dart/lib/unittest/unitest.dart'); |
+ * main() { |
+ * test('this is a test', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * test('this is another test', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * } |
+ * |
+ * Multiple tests, grouped by category: |
+ * |
+ * #import('path-to-dart/lib/unittest/unitest.dart'); |
+ * main() { |
+ * group('group A', () { |
+ * test('test A.1', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * test('test A.2', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * }); |
+ * group('group B', () { |
+ * test('this B.1', () { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }); |
+ * }); |
+ * } |
+ * |
+ * Asynchronous tests: under the current API (soon to be deprecated): |
+ * |
+ * #import('path-to-dart/lib/unittest/unitest.dart'); |
+ * #import('dart:dom'); |
+ * main() { |
+ * // use [asyncTest], indicate the expected number of callbacks: |
+ * asyncTest('this is a test', 1, () { |
+ * window.setTimeout(() { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * // invoke [callbackDone] at the end of the callback. |
+ * callbackDone(); |
+ * }, 0); |
+ * }); |
+ * } |
+ * |
+ * We plan to replace this with a different API, one API we are considering is: |
+ * |
+ * #import('path-to-dart/lib/unittest/unitest.dart'); |
+ * #import('dart:dom'); |
+ * main() { |
+ * test('this is a test', () { |
+ * // wrap the callback of an asynchronous call with [later] |
+ * window.setTimeout(later(() { |
+ * int x = 2 + 3; |
+ * expect(x).equals(5); |
+ * }), 0); |
+ * }); |
+ * } |
+ */ |
+#library('unittest'); |
+ |
+#import('dart:isolate'); |
+ |
+#source('config.dart'); |
+#source('expectation.dart'); |
+#source('test_case.dart'); |
+ |
+/** [Configuration] used by the unittest library. */ |
+Configuration _config = null; |
+ |
+/** Set the [Configuration] used by the unittest library. */ |
+void configure(Configuration config) { |
+ _config = config; |
+} |
+ |
+/** |
+ * Description text of the current test group. If multiple groups are nested, |
+ * this will contain all of their text concatenated. |
+ */ |
+String _currentGroup = ''; |
+ |
+/** Tests executed in this suite. */ |
+List<TestCase> _tests; |
+ |
+/** |
+ * Callback used to run tests. Entrypoints can replace this with their own |
+ * if they want. |
+ */ |
+Function _testRunner; |
+ |
+/** Current test being executed. */ |
+int _currentTest = 0; |
+ |
+/** Total number of callbacks that have been executed in the current test. */ |
+int _callbacksCalled = 0; |
+ |
+final _UNINITIALIZED = 0; |
+final _READY = 1; |
+final _RUNNING_TEST = 2; |
+ |
+/** |
+ * Whether an undetected error occurred while running the last test. These |
+ * errors are commonly caused by DOM callbacks that were not guarded in a |
+ * try-catch block. |
+ */ |
+final _UNCAUGHT_ERROR = 3; |
+ |
+int _state = _UNINITIALIZED; |
+ |
+final _PASS = 'pass'; |
+final _FAIL = 'fail'; |
+final _ERROR = 'error'; |
+ |
+/** Creates an expectation for the given value. */ |
+Expectation expect(value) => new Expectation(value); |
+ |
+/** Evaluates the given function and validates that it throws an exception. */ |
+void expectThrow(function) { |
+ bool threw = false; |
+ try { |
+ function(); |
+ } catch (var e) { |
+ threw = true; |
+ } |
+ Expect.equals(true, threw, 'Expected exception but none was thrown.'); |
+} |
+ |
+/** |
+ * Creates a new test case with the given description and body. The |
+ * description will include the descriptions of any surrounding group() |
+ * calls. |
+ */ |
+void test(String spec, TestFunction body) { |
+ _ensureInitialized(); |
+ |
+ _tests.add(new TestCase(_tests.length + 1, _fullSpec(spec), body, 0)); |
+} |
+ |
+/** |
+ * Creates a new async test case with the given description and body. The |
+ * description will include the descriptions of any surrounding group() |
+ * calls. |
+ */ |
+// TODO(sigmund): deprecate this API |
+void asyncTest(String spec, int callbacks, TestFunction body) { |
+ _ensureInitialized(); |
+ |
+ final testCase = new TestCase( |
+ _tests.length + 1, _fullSpec(spec), body, callbacks); |
+ _tests.add(testCase); |
+ |
+ if (callbacks < 1) { |
+ testCase.error( |
+ 'Async tests must wait for at least one callback ', ''); |
+ } |
+} |
+ |
+class _Sentinel { |
+ static final _sentinel = const _Sentinel(); |
+ |
+ const _Sentinel(); |
+} |
+ |
+ |
+/** |
+ * Indicate to the unittest framework that a callback is expected. [callback] |
+ * can take any number of arguments between 0 and 4. |
+ * |
+ * The framework will wait for the callback to run before it continues with the |
+ * following test. The callback must excute once and only once. Using [later] |
+ * will also ensure that errors that occur within the callback are tracked and |
+ * reported by the unittest framework. |
+ */ |
+// TODO(sigmund): expose this functionality |
+Function _later(Function callback) { |
+ Expect.isTrue(_currentTest < _tests.length); |
+ var testCase = _tests[_currentTest]; |
+ testCase.callbacks++; |
+ // We simulate spread arguments using named arguments: |
+ // Note: this works in the vm and dart2js, but not in frog. |
+ return ([arg0 = _Sentinel.value, arg1 = _Sentinel.value, |
+ arg2 = _Sentinel.value, arg3 = _Sentinel.value, |
+ arg4 = _Sentinel.value]) { |
+ _guard(() { |
+ if (arg0 == _Sentinel.value) { |
+ callback(); |
+ } else if (arg1 == _Sentinel.value) { |
+ callback(arg0); |
+ } else if (arg2 == _Sentinel.value) { |
+ callback(arg0, arg1); |
+ } else if (arg3 == _Sentinel.value) { |
+ callback(arg0, arg1, arg2); |
+ } else if (arg4 == _Sentinel.value) { |
+ callback(arg0, arg1, arg2, arg3); |
+ } else { |
+ testCase.error( |
+ 'unittest lib does not support callbacks with more than 4 arguments', |
+ ''); |
+ _state = _UNCAUGHT_ERROR; |
+ } |
+ }, callbackDone); |
+ }; |
+} |
+ |
+// TODO(sigmund): expose this functionality |
+Function _later0(Function callback) { |
+ Expect.isTrue(_currentTest < _tests.length); |
+ var testCase = _tests[_currentTest]; |
+ testCase.callbacks++; |
+ return () { |
+ _guard(() => callback(), callbackDone); |
+ }; |
+} |
+ |
+// TODO(sigmund): expose this functionality |
+Function _later1(Function callback) { |
+ Expect.isTrue(_currentTest < _tests.length); |
+ var testCase = _tests[_currentTest]; |
+ testCase.callbacks++; |
+ return (arg0) { |
+ _guard(() => callback(arg0), callbackDone); |
+ }; |
+} |
+ |
+// TODO(sigmund): expose this functionality |
+Function _later2(Function callback) { |
+ Expect.isTrue(_currentTest < _tests.length); |
+ var testCase = _tests[_currentTest]; |
+ testCase.callbacks++; |
+ return (arg0, arg1) { |
+ _guard(() => callback(arg0, arg1), callbackDone); |
+ }; |
+} |
+ |
+/** |
+ * Creates a new named group of tests. Calls to group() or test() within the |
+ * body of the function passed to this will inherit this group's description. |
+ */ |
+void group(String description, void body()) { |
+ _ensureInitialized(); |
+ |
+ // Concatenate the new group. |
+ final oldGroup = _currentGroup; |
+ if (_currentGroup != '') { |
+ // Add a space. |
+ _currentGroup = '$_currentGroup $description'; |
+ } else { |
+ // The first group. |
+ _currentGroup = description; |
+ } |
+ |
+ try { |
+ body(); |
+ } finally { |
+ // Now that the group is over, restore the previous one. |
+ _currentGroup = oldGroup; |
+ } |
+} |
+ |
+/** Called by subclasses to indicate that an asynchronous test completed. */ |
+void callbackDone() { |
+ _callbacksCalled++; |
+ final testCase = _tests[_currentTest]; |
+ if (_callbacksCalled > testCase.callbacks) { |
+ final expected = testCase.callbacks; |
+ testCase.error( |
+ 'More calls to callbackDone() than expected. ' |
+ + 'Actual: ${_callbacksCalled}, expected: ${expected}', ''); |
+ _state = _UNCAUGHT_ERROR; |
+ } else if ((_callbacksCalled == testCase.callbacks) && |
+ (_state != _RUNNING_TEST)) { |
+ if (testCase.result == null) testCase.pass(); |
+ _currentTest++; |
+ _testRunner(); |
+ } |
+} |
+ |
+void notifyError(String msg, String trace) { |
+ if (_currentTest < _tests.length) { |
+ final testCase = _tests[_currentTest]; |
+ testCase.error(msg, trace); |
+ _state = _UNCAUGHT_ERROR; |
+ if (testCase.callbacks > 0) { |
+ _currentTest++; |
+ _testRunner(); |
+ } |
+ } |
+} |
+ |
+/** Runs [callback] at the end of the event loop. */ |
+_defer(void callback()) { |
+ // Exploit isolate ports as a platform-independent mechanism to queue a |
+ // message at the end of the event loop. |
+ // TODO(sigmund): expose this functionality somewhere in our libraries. |
+ final port = new ReceivePort(); |
+ port.receive((msg, reply) { |
+ callback(); |
+ port.close(); |
+ }); |
+ port.toSendPort().send(null, null); |
+} |
+ |
+/** Runs all queued tests, one at a time. */ |
+_runTests() { |
+ _config.onStart(); |
+ |
+ _defer(() { |
+ assert (_currentTest == 0); |
+ _testRunner(); |
+ }); |
+} |
+ |
+/** |
+ * Run [tryBody] guarded in a try-catch block. If an exception is thrown, update |
+ * the [_currentTest] status accordingly. |
+ */ |
+_guard(tryBody, [finallyBody]) { |
+ final testCase = _tests[_currentTest]; |
+ try { |
+ tryBody(); |
+ } catch (ExpectException e, var trace) { |
+ if (_state != _UNCAUGHT_ERROR) { |
+ //TODO(pquitslund) remove guard once dartc reliably propagates traces |
+ testCase.fail(e.message, trace == null ? '' : trace.toString()); |
+ } |
+ } catch (var e, var trace) { |
+ if (_state != _UNCAUGHT_ERROR) { |
+ //TODO(pquitslund) remove guard once dartc reliably propagates traces |
+ testCase.error('Caught ${e}', trace == null ? '' : trace.toString()); |
+ } |
+ } finally { |
+ _state = _READY; |
+ if (finallyBody != null) finallyBody(); |
+ } |
+} |
+ |
+/** |
+ * Runs a batch of tests, yielding whenever an asynchronous test starts |
+ * running. Tests will resume executing when such asynchronous test calls |
+ * [done] or if it fails with an exception. |
+ */ |
+_nextBatch() { |
+ while (_currentTest < _tests.length) { |
+ final testCase = _tests[_currentTest]; |
+ |
+ _guard(() { |
+ _callbacksCalled = 0; |
+ _state = _RUNNING_TEST; |
+ |
+ testCase.test(); |
+ |
+ if (_state != _UNCAUGHT_ERROR) { |
+ if (testCase.callbacks == _callbacksCalled) { |
+ testCase.pass(); |
+ } |
+ } |
+ }); |
+ |
+ if (!testCase.isComplete && testCase.callbacks > 0) return; |
+ |
+ _currentTest++; |
+ } |
+ |
+ _completeTests(); |
+} |
+ |
+/** Publish results on the page and notify controller. */ |
+_completeTests() { |
+ _state = _UNINITIALIZED; |
+ |
+ int testsPassed_ = 0; |
+ int testsFailed_ = 0; |
+ int testsErrors_ = 0; |
+ |
+ for (TestCase t in _tests) { |
+ switch (t.result) { |
+ case _PASS: testsPassed_++; break; |
+ case _FAIL: testsFailed_++; break; |
+ case _ERROR: testsErrors_++; break; |
+ } |
+ } |
+ |
+ _config.onDone(testsPassed_, testsFailed_, testsErrors_, _tests); |
+} |
+ |
+String _fullSpec(String spec) { |
+ if (spec === null) return '$_currentGroup'; |
+ return _currentGroup != '' ? '$_currentGroup $spec' : spec; |
+} |
+ |
+/** |
+ * Lazily initializes the test library if not already initialized. |
+ */ |
+_ensureInitialized() { |
+ if (_state != _UNINITIALIZED) return; |
+ |
+ _tests = <TestCase>[]; |
+ _currentGroup = ''; |
+ _state = _READY; |
+ _testRunner = _nextBatch; |
+ |
+ if (_config == null) { |
+ _config = new Configuration(); |
+ } |
+ _config.onInit(); |
+ |
+ // Immediately queue the suite up. It will run after a timeout (i.e. after |
+ // main() has returned). |
+ _defer(_runTests); |
+} |
+ |
+/** Signature for a test function. */ |
+typedef void TestFunction(); |