Index: observatory_pub_packages/unittest/unittest.dart |
=================================================================== |
--- observatory_pub_packages/unittest/unittest.dart (revision 0) |
+++ observatory_pub_packages/unittest/unittest.dart (working copy) |
@@ -0,0 +1,601 @@ |
+// Copyright (c) 2013, 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. |
+ |
+/// Support for writing Dart unit tests. |
+/// |
+/// For information on installing and importing this library, see the |
+/// [unittest package on pub.dartlang.org] |
+/// (http://pub.dartlang.org/packages/unittest). |
+/// |
+/// **See also:** |
+/// [Unit Testing with Dart] |
+/// (http://www.dartlang.org/articles/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] |
+/// |
+/// * __Matchers__: [expect] assertions are written declaratively using the |
+/// [Matcher] class. |
+/// |
+/// * __Configuration__: The framework can be adapted by setting |
+/// [unittestConfiguration] with a [Configuration]. See the other libraries |
+/// in the `unittest` package for alternative implementations of |
+/// [Configuration] including `compact_vm_config.dart`, `html_config.dart` |
+/// and `html_enhanced_config.dart`. |
+/// |
+/// ##Examples |
+/// |
+/// A trivial test: |
+/// |
+/// import 'package:unittest/unittest.dart'; |
+/// main() { |
+/// test('this is a test', () { |
+/// int x = 2 + 3; |
+/// expect(x, equals(5)); |
+/// }); |
+/// } |
+/// |
+/// Multiple tests: |
+/// |
+/// import 'package:unittest/unittest.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 'package:unittest/unittest.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: if callbacks expect between 0 and 6 positional |
+/// arguments, [expectAsync] will wrap a function into a new callback and will |
+/// not consider the test complete until that callback is run. A count argument |
+/// can be provided to specify the number of times the callback should be called |
+/// (the default is 1). |
+/// |
+/// import 'dart:async'; |
+/// import 'package:unittest/unittest.dart'; |
+/// void main() { |
+/// test('callback is executed once', () { |
+/// // wrap the callback of an asynchronous call with [expectAsync] if |
+/// // the callback takes 0 arguments... |
+/// var timer = Timer.run(expectAsync(() { |
+/// int x = 2 + 3; |
+/// expect(x, equals(5)); |
+/// })); |
+/// }); |
+/// |
+/// test('callback is executed twice', () { |
+/// var callback = expectAsync(() { |
+/// int x = 2 + 3; |
+/// expect(x, equals(5)); |
+/// }, count: 2); // <-- we can indicate multiplicity to [expectAsync] |
+/// Timer.run(callback); |
+/// Timer.run(callback); |
+/// }); |
+/// } |
+/// |
+/// There may be times when the number of times a callback should be called is |
+/// non-deterministic. In this case a dummy callback can be created with |
+/// expectAsync((){}) and this can be called from the real callback when it is |
+/// finally complete. |
+/// |
+/// A variation on this is [expectAsyncUntil], which takes a callback as the |
+/// first parameter and a predicate function as the second parameter. After each |
+/// time the callback is called, the predicate function will be called. If it |
+/// returns `false` the test will still be considered incomplete. |
+/// |
+/// Test functions can return [Future]s, which provide another way of doing |
+/// asynchronous tests. The test framework will handle exceptions thrown by |
+/// the Future, and will advance to the next test when the Future is complete. |
+/// |
+/// import 'dart:async'; |
+/// import 'package:unittest/unittest.dart'; |
+/// void main() { |
+/// test('test that time has passed', () { |
+/// var duration = const Duration(milliseconds: 200); |
+/// var time = new DateTime.now(); |
+/// |
+/// return new Future.delayed(duration).then((_) { |
+/// var delta = new DateTime.now().difference(time); |
+/// |
+/// expect(delta, greaterThanOrEqualTo(duration)); |
+/// }); |
+/// }); |
+/// } |
+library unittest; |
+ |
+import 'dart:async'; |
+import 'dart:collection'; |
+import 'dart:isolate'; |
+ |
+import 'package:matcher/matcher.dart' show DefaultFailureHandler, |
+ configureExpectFailureHandler, TestFailure, wrapAsync; |
+export 'package:matcher/matcher.dart'; |
+ |
+import 'src/utils.dart'; |
+ |
+import 'src/configuration.dart'; |
+export 'src/configuration.dart'; |
+ |
+part 'src/simple_configuration.dart'; |
+part 'src/group_context.dart'; |
+part 'src/spread_args_helper.dart'; |
+part 'src/test_case.dart'; |
+part 'src/test_environment.dart'; |
+ |
+const Symbol _UNITTEST_ENVIRONMENT = #unittest.environment; |
+ |
+final _TestEnvironment _defaultEnvironment = new _TestEnvironment(); |
+ |
+/** |
+ * Internal getter for the current unittest config. |
+ */ |
+_TestEnvironment get _environment { |
+ var environment = Zone.current[_UNITTEST_ENVIRONMENT]; |
+ if (environment == null) return _defaultEnvironment; |
+ return environment; |
+} |
+ |
+// Convenience getter for the current environment's config. |
+Configuration get _config => _environment.config; |
+ |
+// Convenience setter for the current environment's config. |
+void set _config(Configuration config) { |
+ _environment.config = config; |
+} |
+ |
+// Convenience getter for the current environment's test cases. |
+List<TestCase> get _testCases => _environment.testCases; |
+ |
+/// [Configuration] used by the unittest library. |
+/// |
+/// Note that if a configuration has not been set, calling this getter will |
+/// create a default configuration. |
+Configuration get unittestConfiguration { |
+ if (_config == null) { |
+ _config = new Configuration(); |
+ } |
+ return _config; |
+} |
+ |
+/// Sets the [Configuration] used by the unittest library. |
+/// |
+/// Throws a [StateError] if there is an existing, incompatible value. |
+void set unittestConfiguration(Configuration value) { |
+ if (!identical(_config, value)) { |
+ if (_config != null) { |
+ logMessage('Warning: The unittestConfiguration has already been set. New ' |
+ 'unittestConfiguration ignored.'); |
+ } else { |
+ _config = value; |
+ } |
+ } |
+} |
+ |
+/// Can be called by tests to log status. Tests should use this |
+/// instead of [print]. |
+void logMessage(String message) => |
+ _config.onLogMessage(currentTestCase, message); |
+ |
+/// Separator used between group names and test names. |
+String groupSep = ' '; |
+ |
+/// Tests executed in this suite. |
+List<TestCase> get testCases => |
+ new UnmodifiableListView<TestCase>(_environment.testCases); |
+ |
+/// Interval (in msecs) after which synchronous tests will insert an async |
+/// delay to allow DOM or other updates. |
+const int BREATH_INTERVAL = 200; |
+ |
+/// [TestCase] currently being executed. |
+TestCase get currentTestCase => |
+ (_environment.currentTestCaseIndex >= 0 && |
+ _environment.currentTestCaseIndex < testCases.length) |
+ ? testCases[_environment.currentTestCaseIndex] |
+ : null; |
+ |
+/* Test case result strings. */ |
+// TODO(gram) we should change these constants to use a different string |
+// (so that writing 'FAIL' in the middle of a test doesn't |
+// imply that the test fails). We can't do it without also changing |
+// the testrunner and test.dart though. |
+/// Result string for a passing test case. |
+const PASS = 'pass'; |
+/// Result string for a failing test case. |
+const FAIL = 'fail'; |
+/// Result string for an test case with an error. |
+const ERROR = 'error'; |
+ |
+/// 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) { |
+ _requireNotRunning(); |
+ ensureInitialized(); |
+ if (!_environment.soloTestSeen || _environment.soloNestingLevel > 0) { |
+ var testcase = new TestCase._internal(testCases.length + 1, _fullSpec(spec), |
+ body); |
+ _testCases.add(testcase); |
+ } |
+} |
+ |
+/// Convenience function for skipping a test. |
+void skip_test(String spec, TestFunction body) {} |
+ |
+/// Creates a new test case with the given description and body. The |
+/// description will include the descriptions of any surrounding group() |
+/// calls. |
+/// |
+/// If we use [solo_test] (or [solo_group]) instead of test, then all non-solo |
+/// tests will be disabled. Note that if we use [solo_group], all tests in |
+/// the group will be enabled, regardless of whether they use [test] or |
+/// [solo_test], or whether they are in a nested [group] vs [solo_group]. Put |
+/// another way, if there are any calls to [solo_test] or [solo_group] in a test |
+/// file, all tests that are not inside a [solo_group] will be disabled unless |
+/// they are [solo_test]s. |
+/// |
+/// [skip_test] and [skip_group] take precedence over soloing, by virtue of the |
+/// fact that they are effectively no-ops. |
+void solo_test(String spec, TestFunction body) { |
+ _requireNotRunning(); |
+ ensureInitialized(); |
+ if (!_environment.soloTestSeen) { |
+ _environment.soloTestSeen = true; |
+ // This is the first solo-ed test. Discard all tests up to now. |
+ _testCases.clear(); |
+ } |
+ ++_environment.soloNestingLevel; |
+ try { |
+ test(spec, body); |
+ } finally { |
+ --_environment.soloNestingLevel; |
+ } |
+} |
+ |
+/// Indicate that [callback] is expected to be called a [count] number of times |
+/// (by default 1). |
+/// |
+/// The unittest framework will wait for the callback to run the |
+/// specified [count] times before it continues with the following test. Using |
+/// [expectAsync] will also ensure that errors that occur within [callback] are |
+/// tracked and reported. [callback] should take 0 positional arguments (named |
+/// arguments are not supported). [id] can be used to provide more |
+/// descriptive error messages if the callback is called more often than |
+/// expected. |
+/// |
+/// [max] can be used to specify an upper bound on the number of |
+/// calls; if this is exceeded the test will fail (or be marked as in error if |
+/// it was already complete). A value of 0 for [max] (the default) will set |
+/// the upper bound to the same value as [count]; i.e. the callback should be |
+/// called exactly [count] times. A value of -1 for [max] will mean no upper |
+/// bound. |
+/// |
+/// [reason] is optional and is typically not supplied, as a reason is generated |
+/// by the unittest package; if reason is included it is appended to the |
+/// generated reason. |
+Function expectAsync(Function callback, |
+ {int count: 1, int max: 0, String id, String reason}) => |
+ new _SpreadArgsHelper(callback, count, max, id, reason).func; |
+ |
+/// Indicate that [callback] is expected to be called until [isDone] returns |
+/// true. |
+/// |
+/// The unittest framework checks [isDone] after each callback and only |
+/// when it returns true will it continue with the following test. Using |
+/// [expectAsyncUntil] will also ensure that errors that occur within |
+/// [callback] are tracked and reported. [callback] should take 0 positional |
+/// arguments (named arguments are not supported). [id] can be used to |
+/// identify the callback in error messages (for example if it is called |
+/// after the test case is complete). |
+/// |
+/// [reason] is optional and is typically not supplied, as a reason is generated |
+/// by the unittest package; if reason is included it is appended to the |
+/// generated reason. |
+Function expectAsyncUntil(Function callback, bool isDone(), |
+ {String id, String reason}) => |
+ new _SpreadArgsHelper(callback, 0, -1, id, reason, isDone: isDone).func; |
+ |
+/// Creates a new named group of tests. |
+/// |
+/// Calls to group() or test() within the body of the function passed to this |
+/// named group will inherit this group's description. |
+void group(String description, void body()) { |
+ ensureInitialized(); |
+ _requireNotRunning(); |
+ _environment.currentContext = |
+ new _GroupContext(_environment.currentContext, description); |
+ try { |
+ body(); |
+ } catch (e, trace) { |
+ var stack = (trace == null) ? '' : ': ${trace.toString()}'; |
+ _environment.uncaughtErrorMessage = "${e.toString()}$stack"; |
+ } finally { |
+ // Now that the group is over, restore the previous one. |
+ _environment.currentContext = _environment.currentContext.parent; |
+ } |
+} |
+ |
+/// Like [skip_test], but for groups. |
+void skip_group(String description, void body()) {} |
+ |
+/// Like [solo_test], but for groups. |
+void solo_group(String description, void body()) { |
+ _requireNotRunning(); |
+ ensureInitialized(); |
+ if (!_environment.soloTestSeen) { |
+ _environment.soloTestSeen = true; |
+ // This is the first solo-ed group. Discard all tests up to now. |
+ _testCases.clear(); |
+ } |
+ ++_environment.soloNestingLevel; |
+ try { |
+ group(description, body); |
+ } finally { |
+ --_environment.soloNestingLevel; |
+ } |
+} |
+ |
+/// Register a [setUp] function for a test [group]. |
+/// |
+/// This function will be called before each test in the group is run. |
+/// [setUp] and [tearDown] should be called within the [group] before any |
+/// calls to [test]. The [setupTest] function can be asynchronous; in this |
+/// case it must return a [Future]. |
+void setUp(Function setupTest) { |
+ _requireNotRunning(); |
+ _environment.currentContext.testSetup = setupTest; |
+} |
+ |
+/// Register a [tearDown] function for a test [group]. |
+/// |
+/// This function will be called after each test in the group is run. |
+/// |
+/// Note that if groups are nested only the most locally scoped [teardownTest] |
+/// function will be run. [setUp] and [tearDown] should be called within the |
+/// [group] before any calls to [test]. The [teardownTest] function can be |
+/// asynchronous; in this case it must return a [Future]. |
+void tearDown(Function teardownTest) { |
+ _requireNotRunning(); |
+ _environment.currentContext.testTeardown = teardownTest; |
+} |
+ |
+/// Advance to the next test case. |
+void _nextTestCase() { |
+ _environment.currentTestCaseIndex++; |
+ _runTest(); |
+} |
+ |
+/// Handle errors that happen outside the tests. |
+// TODO(vsm): figure out how to expose the stack trace here |
+// Currently e.message works in dartium, but not in dartc. |
+void handleExternalError(e, String message, [stack]) { |
+ var msg = '$message\nCaught $e'; |
+ |
+ if (currentTestCase != null) { |
+ currentTestCase._error(msg, stack); |
+ } else { |
+ _environment.uncaughtErrorMessage = "$msg: $stack"; |
+ } |
+} |
+ |
+/// Filter the tests by [testFilter]. |
+/// |
+/// [testFilter] can be a [RegExp], a [String] or a |
+/// predicate function. This is different from enabling or disabling tests |
+/// in that it removes the tests completely. |
+void filterTests(testFilter) { |
+ var filterFunction; |
+ if (testFilter is String) { |
+ RegExp re = new RegExp(testFilter); |
+ filterFunction = (t) => re.hasMatch(t.description); |
+ } else if (testFilter is RegExp) { |
+ filterFunction = (t) => testFilter.hasMatch(t.description); |
+ } else if (testFilter is Function) { |
+ filterFunction = testFilter; |
+ } |
+ _testCases.retainWhere(filterFunction); |
+} |
+ |
+/// Runs all queued tests, one at a time. |
+void runTests() { |
+ _requireNotRunning(); |
+ _ensureInitialized(false); |
+ _environment.currentTestCaseIndex = 0; |
+ _config.onStart(); |
+ _runTest(); |
+} |
+ |
+/// Registers that an exception was caught for the current test. |
+void registerException(e, [trace]) { |
+ _registerException(currentTestCase, e, trace); |
+} |
+ |
+/// Registers that an exception was caught for the current test. |
+void _registerException(TestCase testCase, e, [trace]) { |
+ String message = (e is TestFailure) ? e.message : 'Caught $e'; |
+ if (testCase.result == null) { |
+ testCase._fail(message, trace); |
+ } else { |
+ testCase._error(message, trace); |
+ } |
+} |
+ |
+/// Runs the next test. |
+void _runTest() { |
+ if (_environment.currentTestCaseIndex >= testCases.length) { |
+ assert(_environment.currentTestCaseIndex == testCases.length); |
+ _completeTests(); |
+ } else { |
+ var testCase = testCases[_environment.currentTestCaseIndex]; |
+ Future f = runZoned(testCase._run, onError: (error, stack) { |
+ // TODO(kevmoo) Do a better job of flagging these are async errors. |
+ // https://code.google.com/p/dart/issues/detail?id=16530 |
+ _registerException(testCase, error, stack); |
+ }); |
+ |
+ var timeout = unittestConfiguration.timeout; |
+ |
+ Timer timer; |
+ if (timeout != null) { |
+ try { |
+ timer = new Timer(timeout, () { |
+ testCase._error("Test timed out after ${timeout.inSeconds} seconds."); |
+ _nextTestCase(); |
+ }); |
+ } on UnsupportedError catch (e) { |
+ if (e.message != "Timer greater than 0.") rethrow; |
+ // Support running on d8 and jsshell which don't support timers. |
+ } |
+ } |
+ f.whenComplete(() { |
+ if (timer != null) timer.cancel(); |
+ var now = new DateTime.now().millisecondsSinceEpoch; |
+ if ((now - _environment.lastBreath) >= BREATH_INTERVAL) { |
+ _environment.lastBreath = now; |
+ Timer.run(_nextTestCase); |
+ } else { |
+ scheduleMicrotask(_nextTestCase); // Schedule the next test. |
+ } |
+ }); |
+ } |
+} |
+ |
+/// Publish results on the page and notify controller. |
+void _completeTests() { |
+ if (!_environment.initialized) return; |
+ int passed = 0; |
+ int failed = 0; |
+ int errors = 0; |
+ |
+ for (TestCase t in testCases) { |
+ switch (t.result) { |
+ case PASS: passed++; break; |
+ case FAIL: failed++; break; |
+ case ERROR: errors++; break; |
+ } |
+ } |
+ _config.onSummary(passed, failed, errors, testCases, |
+ _environment.uncaughtErrorMessage); |
+ _config.onDone(passed > 0 && failed == 0 && errors == 0 && |
+ _environment.uncaughtErrorMessage == null); |
+ _environment.initialized = false; |
+ _environment.currentTestCaseIndex = -1; |
+} |
+ |
+String _fullSpec(String spec) { |
+ var group = '${_environment.currentContext.fullName}'; |
+ if (spec == null) return group; |
+ return group != '' ? '$group$groupSep$spec' : spec; |
+} |
+ |
+/// Lazily initializes the test library if not already initialized. |
+void ensureInitialized() { |
+ _ensureInitialized(true); |
+} |
+ |
+void _ensureInitialized(bool configAutoStart) { |
+ if (_environment.initialized) { |
+ return; |
+ } |
+ _environment.initialized = true; |
+ // Hook our async guard into the matcher library. |
+ wrapAsync = (f, [id]) => expectAsync(f, id: id); |
+ |
+ _environment.uncaughtErrorMessage = null; |
+ |
+ unittestConfiguration.onInit(); |
+ |
+ if (configAutoStart && _config.autoStart) { |
+ // Immediately queue the suite up. It will run after a timeout (i.e. after |
+ // main() has returned). |
+ scheduleMicrotask(runTests); |
+ } |
+} |
+ |
+/// Select a solo test by ID. |
+void setSoloTest(int id) => _testCases.retainWhere((t) => t.id == id); |
+ |
+/// Enable/disable a test by ID. |
+void _setTestEnabledState(int testId, bool state) { |
+ // Try fast path first. |
+ if (testCases.length > testId && testCases[testId].id == testId) { |
+ testCases[testId]._enabled = state; |
+ } else { |
+ for (var i = 0; i < testCases.length; i++) { |
+ if (testCases[i].id == testId) { |
+ testCases[i]._enabled = state; |
+ break; |
+ } |
+ } |
+ } |
+} |
+ |
+/// Enable a test by ID. |
+void enableTest(int testId) => _setTestEnabledState(testId, true); |
+ |
+/// Disable a test by ID. |
+void disableTest(int testId) => _setTestEnabledState(testId, false); |
+ |
+/// Signature for a test function. |
+typedef dynamic TestFunction(); |
+ |
+/// A flag that controls whether we hide unittest and core library details in |
+/// exception stacks. |
+/// |
+/// Useful to disable when debugging unittest or matcher customizations. |
+bool formatStacks = true; |
+ |
+/// A flag that controls whether we try to filter out irrelevant frames from |
+/// the stack trace. |
+/// |
+/// Requires [formatStacks] to be set. |
+bool filterStacks = true; |
+ |
+void _requireNotRunning() { |
+ if (_environment.currentTestCaseIndex != -1) { |
+ throw new StateError('Not allowed when tests are running.'); |
+ } |
+} |
+ |
+/// Method to create a test environment running in its own zone scope. |
+/// |
+/// This allows for multiple invocations of the unittest library in the same |
+/// application instance. |
+/// This is useful when, for example, creating a test runner application which |
+/// needs to create a new pristine test environment on each invocation to run |
+/// a given set of test. |
+dynamic withTestEnvironment(callback()) { |
+ return runZoned(callback, |
+ zoneValues: {_UNITTEST_ENVIRONMENT: new _TestEnvironment()}); |
+} |