diff --git a/CHANGELOG.md b/CHANGELOG.md index a8210d913467fd08e82e5eecb1eb0eea5ebeccf8..be183d2c7a7cd084d8b2bb41125ce12414059b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ * Removed several members from `SimpleConfiguration` that relied on removed functionality: `onExpectFailure`, `stopTestOnExpectFailure`, and 'name'. +##0.11.5+1 + +* Internal code cleanups and documentation improvements. + ##0.11.5 * Bumped the version constraint for `matcher`. diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index f4a28c989c73067c370ad45524ab30ff4861bc4f..d3759667241e48d2bdadbcb66f4ba019252eeef5 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -4,12 +4,12 @@ library unittest.configuration; -import 'package:unittest/unittest.dart' show TestCase, SimpleConfiguration; +import 'simple_configuration.dart'; +import 'test_case.dart'; /// Describes the interface used by the unit test system for communicating the /// results of a test run. abstract class Configuration { - /// Creates an instance of [SimpleConfiguration]. factory Configuration() => new SimpleConfiguration(); @@ -18,9 +18,11 @@ abstract class Configuration { /// For use by subclasses which wish to implement only a subset of features. Configuration.blank(); - /// If [:true:], tests are started automatically. Otherwise [runTests] - /// must be called explicitly after tests are set up. - bool get autoStart => true; + /// If `true`, tests are started automatically once they're finished being + /// defined. + /// + /// Otherwise, [runTests] must be called explicitly after tests are set up. + final autoStart = true; /// How long a [TestCase] can run before it is considered an error. /// A [timeout] value of [:null:] means that the limit is infinite. diff --git a/lib/src/expected_function.dart b/lib/src/expected_function.dart new file mode 100644 index 0000000000000000000000000000000000000000..7373c7550829fdb9b010a9a71de3ef77bf9f65a4 --- /dev/null +++ b/lib/src/expected_function.dart @@ -0,0 +1,203 @@ +// Copyright (c) 2015, 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. + +library unittest.expected_function; + +import '../unittest.dart'; + +import 'internal_test_case.dart'; + +/// An object used to detect unpassed arguments. +const _PLACEHOLDER = const Object(); + +// Functions used to check how many arguments a callback takes. +typedef _Func0(); +typedef _Func1(a); +typedef _Func2(a, b); +typedef _Func3(a, b, c); +typedef _Func4(a, b, c, d); +typedef _Func5(a, b, c, d, e); +typedef _Func6(a, b, c, d, e, f); + +typedef bool _IsDoneCallback(); + +/// A wrapper for a function that ensures that it's called the appropriate +/// number of times. +/// +/// The containing test won't be considered to have completed successfully until +/// this function has been called the appropriate number of times. +/// +/// The wrapper function is accessible via [func]. It supports up to six +/// optional and/or required positional arguments, but no named arguments. +class ExpectedFunction { + /// The wrapped callback. + final Function _callback; + + /// The minimum number of calls that are expected to be made to the function. + /// + /// If fewer calls than this are made, the test will fail. + final int _minExpectedCalls; + + /// The maximum number of calls that are expected to be made to the function. + /// + /// If more calls than this are made, the test will fail. + final int _maxExpectedCalls; + + /// A callback that should return whether the function is not expected to have + /// any more calls. + /// + /// This will be called after every time the function is run. The test case + /// won't be allowed to terminate until it returns `true`. + /// + /// This may be `null`. If so, the function is considered to be done after + /// it's been run once. + final _IsDoneCallback _isDone; + + /// A descriptive name for the function. + final String _id; + + /// An optional description of why the function is expected to be called. + /// + /// If not passed, this will be an empty string. + final String _reason; + + /// The number of times the function has been called. + int _actualCalls = 0; + + /// The test case in which this function was wrapped. + final InternalTestCase _testCase; + + /// Whether this function has been called the requisite number of times. + bool _complete; + + /// Wraps [callback] in a function that asserts that it's called at least + /// [minExpected] times and no more than [maxExpected] times. + /// + /// If passed, [id] is used as a descriptive name fo the function and [reason] + /// as a reason it's expected to be called. If [isDone] is passed, the test + /// won't be allowed to complete until it returns `true`. + ExpectedFunction(Function callback, int minExpected, int maxExpected, + {String id, String reason, bool isDone()}) + : this._callback = callback, + _minExpectedCalls = minExpected, + _maxExpectedCalls = (maxExpected == 0 && minExpected > 0) + ? minExpected + : maxExpected, + this._isDone = isDone, + this._reason = reason == null ? '' : '\n$reason', + this._testCase = currentTestCase as InternalTestCase, + this._id = _makeCallbackId(id, callback) { + ensureInitialized(); + if (_testCase == null) { + throw new StateError("No valid test. Did you forget to run your test " + "inside a call to test()?"); + } + + if (isDone != null || minExpected > 0) { + _testCase.callbackFunctionsOutstanding++; + _complete = false; + } else { + _complete = true; + } + } + + /// Tries to find a reasonable name for [callback]. + /// + /// If [id] is passed, uses that. Otherwise, tries to determine a name from + /// calling `toString`. If no name can be found, returns the empty string. + static String _makeCallbackId(String id, Function callback) { + if (id != null) return "$id "; + + // If the callback is not an anonymous closure, try to get the + // name. + var toString = callback.toString(); + var prefix = "Function '"; + var start = toString.indexOf(prefix); + if (start == -1) return ''; + + start += prefix.length; + var end = toString.indexOf("'", start); + if (end == -1) return ''; + return "${toString.substring(start, end)} "; + } + + /// Returns a function that has the same number of positional arguments as the + /// wrapped function (up to a total of 6). + Function get func { + if (_callback is _Func6) return _max6; + if (_callback is _Func5) return _max5; + if (_callback is _Func4) return _max4; + if (_callback is _Func3) return _max3; + if (_callback is _Func2) return _max2; + if (_callback is _Func1) return _max1; + if (_callback is _Func0) return _max0; + + throw new ArgumentError( + 'The wrapped function has more than 6 required arguments'); + } + + // This indirection is critical. It ensures the returned function has an + // argument count of zero. + _max0() => _max6(); + + _max1([a0 = _PLACEHOLDER]) => _max6(a0); + + _max2([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER]) => _max6(a0, a1); + + _max3([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER]) => + _max6(a0, a1, a2); + + _max4([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, + a3 = _PLACEHOLDER]) => _max6(a0, a1, a2, a3); + + _max5([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, + a3 = _PLACEHOLDER, a4 = _PLACEHOLDER]) => _max6(a0, a1, a2, a3, a4); + + _max6([a0 = _PLACEHOLDER, a1 = _PLACEHOLDER, a2 = _PLACEHOLDER, + a3 = _PLACEHOLDER, a4 = _PLACEHOLDER, a5 = _PLACEHOLDER]) => + _run([a0, a1, a2, a3, a4, a5].where((a) => a != _PLACEHOLDER)); + + /// Runs the wrapped function with [args] and returns its return value. + /// + /// This will pass any errors on to [_testCase] and return `null`. + _run(Iterable args) { + try { + _actualCalls++; + if (_testCase.isComplete) { + // Don't run the callback if the test is done. We don't throw here as + // this is not the current test, but we do mark the old test as having + // an error if it previously passed. + if (_testCase.result == PASS) { + _testCase.error( + 'Callback ${_id}called ($_actualCalls) after test case ' + '${_testCase.description} had already been marked as ' + '${_testCase.result}.$_reason'); + } + return null; + } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) { + throw new TestFailure('Callback ${_id}called more times than expected ' + '($_maxExpectedCalls).$_reason'); + } + + return Function.apply(_callback, args.toList()); + } catch (error, stackTrace) { + _testCase.registerException(error, stackTrace); + return null; + } finally { + _afterRun(); + } + } + + /// After each time the function is run, check to see if it's complete. + void _afterRun() { + if (_complete) return; + if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return; + if (_isDone != null && !_isDone()) return; + + // Mark this callback as complete and remove it from the test case's + // oustanding callback count; if that hits zero the test is done. + _complete = true; + _testCase.markCallbackComplete(); + } +} diff --git a/lib/src/group_context.dart b/lib/src/group_context.dart index d44b6cd3fec4a21a7c82ab2795b99fa5cd06e5ce..78f347b4d32328caddb35b7ab6f542de4204d5f2 100644 --- a/lib/src/group_context.dart +++ b/lib/src/group_context.dart @@ -1,65 +1,75 @@ -part of unittest; +// Copyright (c) 2015, 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. + +library unittest.group_context; + +import 'dart:async'; + +import '../unittest.dart'; /// Setup and teardown functions for a group and its parents, the latter /// for chaining. -class _GroupContext { - final _GroupContext parent; +class GroupContext { + /// The parent context, or `null`. + final GroupContext parent; + + /// Whether this is the root context. + bool get isRoot => parent == null; /// Description text of the current test group. final String _name; - /// Setup function called before each test in a group. - Function _testSetup; - - get testSetup => _testSetup; - - get parentSetup => (parent == null) ? null : parent.testSetup; + /// The set-up function called before each test in a group. + Function get testSetUp => _testSetUp; + Function _testSetUp; - set testSetup(Function setup) { - var preSetup = parentSetup; - if (preSetup == null) { - _testSetup = setup; - } else { - _testSetup = () { - var f = preSetup(); - if (f is Future) { - return f.then((_) => setup()); - } else { - return setup(); - } - }; + set testSetUp(Function setUp) { + if (parent == null || parent.testSetUp == null) { + _testSetUp = setUp; + return; } - } - - /// Teardown function called after each test in a group. - Function _testTeardown; - get testTeardown => _testTeardown; + _testSetUp = () { + var f = parent.testSetUp(); + if (f is Future) { + return f.then((_) => setUp()); + } else { + return setUp(); + } + }; + } - get parentTeardown => (parent == null) ? null : parent.testTeardown; + /// The tear-down function called after each test in a group. + Function get testTearDown => _testTearDown; + Function _testTearDown; - set testTeardown(Function teardown) { - var postTeardown = parentTeardown; - if (postTeardown == null) { - _testTeardown = teardown; - } else { - _testTeardown = () { - var f = teardown(); - if (f is Future) { - return f.then((_) => postTeardown()); - } else { - return postTeardown(); - } - }; + set testTearDown(Function tearDown) { + if (parent == null || parent.testTearDown == null) { + _testTearDown = tearDown; + return; } + + _testTearDown = () { + var f = tearDown(); + if (f is Future) { + return f.then((_) => parent.testTearDown()); + } else { + return parent.testTearDown(); + } + }; } - String get fullName => (parent == null || parent == _environment.rootContext) - ? _name - : "${parent.fullName}$groupSep$_name"; + /// Returns the fully-qualified name of this context. + String get fullName => + (isRoot || parent.isRoot) ? _name : "${parent.fullName}$groupSep$_name"; + + GroupContext.root() + : parent = null, + _name = ''; - _GroupContext([this.parent, this._name = '']) { - _testSetup = parentSetup; - _testTeardown = parentTeardown; + GroupContext(this.parent, this._name) { + _testSetUp = parent.testSetUp; + _testTearDown = parent.testTearDown; } } diff --git a/lib/src/internal_test_case.dart b/lib/src/internal_test_case.dart new file mode 100644 index 0000000000000000000000000000000000000000..1227eea2f58146d96f86632a9919c9e0190a1c15 --- /dev/null +++ b/lib/src/internal_test_case.dart @@ -0,0 +1,227 @@ +// Copyright (c) 2014, 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. + +library unittest.internal_test_case; + +import 'dart:async'; + +import '../unittest.dart'; +import 'test_environment.dart'; +import 'utils.dart'; + +/// An implementation of [TestCase] that exposes internal properties for other +/// unittest use. +class InternalTestCase implements TestCase { + final int id; + final String description; + + /// The setup function to call before the test, if any. + Function _setUp; + + /// The teardown function to call after the test, if any. + Function _tearDown; + + /// The body of the test case. + TestFunction _testFunction; + + /// Remaining number of callback functions that must reach a 'done' state + /// before the test completes. + int callbackFunctionsOutstanding = 0; + + /// The error or failure message for the tests. + /// + /// Initially an empty string. + String message = ''; + + /// The result of the test case. + /// + /// If the test case has is completed, this will be one of [PASS], [FAIL], or + /// [ERROR]. Otherwise, it will be `null`. + String result; + + /// Returns whether this test case passed. + bool get passed => result == PASS; + + /// The stack trace for the error that caused this test case to fail, or + /// `null` if it succeeded. + StackTrace stackTrace; + + /// The name of the group within which this test is running. + final String currentGroup; + + /// The time the test case started running. + /// + /// `null` if the test hasn't yet begun running. + DateTime get startTime => _startTime; + DateTime _startTime; + + /// The amount of time the test case took. + /// + /// `null` if the test hasn't finished running. + Duration get runningTime => _runningTime; + Duration _runningTime; + + /// Whether this test is enabled. + /// + /// Disabled tests won't be run. + bool enabled = true; + + /// A completer that will complete when the test is finished. + /// + /// This is only non-`null` when outstanding callbacks exist. + Completer _testComplete; + + /// Whether this test case has finished running. + bool get isComplete => !enabled || result != null; + + InternalTestCase(this.id, this.description, this._testFunction) + : currentGroup = environment.currentContext.fullName, + _setUp = environment.currentContext.testSetUp, + _tearDown = environment.currentContext.testTearDown; + + /// A function that returns another function to handle errors from [Future]s. + /// + /// [stage] is a string description of the stage of testing that failed. + Function _errorHandler(String stage) => (e, stack) { + if (stack == null && e is Error) { + stack = e.stackTrace; + } + if (result == null || result == PASS) { + if (e is TestFailure) { + fail("$e", stack); + } else { + error("$stage failed: Caught $e", stack); + } + } + }; + + /// Performs any associated [_setUp] function and runs the test. + /// + /// Returns a [Future] that can be used to schedule the next test. If the test + /// runs to completion synchronously, or is disabled, null is returned, to + /// tell unittest to schedule the next test immediately. + Future run() { + if (!enabled) return new Future.value(); + + result = stackTrace = null; + message = ''; + + // Avoid calling [new Future] to avoid issue 11911. + return new Future.value().then((_) { + if (_setUp != null) return _setUp(); + }).catchError(_errorHandler('Setup')).then((_) { + // Skip the test if setup failed. + if (result != null) return new Future.value(); + config.onTestStart(this); + _startTime = new DateTime.now(); + _runningTime = null; + callbackFunctionsOutstanding++; + var testReturn = _testFunction(); + // If _testFunction() returned a future, we want to wait for it like we + // would a callback, so if a failure occurs while waiting, we can abort. + if (testReturn is Future) { + callbackFunctionsOutstanding++; + testReturn + .catchError(_errorHandler('Test')) + .whenComplete(markCallbackComplete); + } + }).catchError(_errorHandler('Test')).then((_) { + markCallbackComplete(); + if (result == null) { + // Outstanding callbacks exist; we need to return a Future. + _testComplete = new Completer(); + return _testComplete.future.whenComplete(() { + if (_tearDown != null) { + return _tearDown(); + } + }).catchError(_errorHandler('Teardown')); + } else if (_tearDown != null) { + return _tearDown(); + } + }).catchError(_errorHandler('Teardown')).whenComplete(() { + _setUp = null; + _tearDown = null; + _testFunction = null; + }); + } + + /// Marks the test as having completed with [testResult], which should be one + /// of [PASS], [FAIL], or [ERROR]. + void _complete(String testResult, + [String messageText = '', StackTrace stack]) { + if (runningTime == null) { + // The startTime can be `null` if an error happened during setup. In this + // case we simply report a running time of 0. + if (startTime != null) { + _runningTime = new DateTime.now().difference(startTime); + } else { + _runningTime = const Duration(seconds: 0); + } + } + _setResult(testResult, messageText, stack); + if (_testComplete != null) { + var t = _testComplete; + _testComplete = null; + t.complete(this); + } + } + + // Sets [this]'s fields to reflect the test result, and notifies the current + // configuration that the test has completed. + // + // Returns true if this is the first time the result has been set. + void _setResult(String testResult, String messageText, StackTrace stack) { + message = messageText; + stackTrace = getTrace(stack, formatStacks, filterStacks); + if (stackTrace == null) stackTrace = stack; + if (result == null) { + result = testResult; + config.onTestResult(this); + } else { + result = testResult; + config.onTestResultChanged(this); + } + } + + /// Marks the test as having passed. + void pass() { + _complete(PASS); + } + + void registerException(error, [StackTrace stackTrace]) { + var message = error is TestFailure ? error.message : 'Caught $error'; + if (result == null) { + fail(message, stackTrace); + } else { + error(message, stackTrace); + } + } + + /// Marks the test as having failed. + void fail(String messageText, [StackTrace stack]) { + if (result != null) { + var newMessage = result == PASS + ? 'Test failed after initially passing: $messageText' + : 'Test failed more than once: $messageText'; + // TODO(gram): Should we combine the stack with the old one? + _complete(ERROR, newMessage, stack); + } else { + _complete(FAIL, messageText, stack); + } + } + + /// Marks the test as having had an unexpected error. + void error(String messageText, [StackTrace stack]) { + _complete(ERROR, messageText, stack); + } + + /// Indicates that an asynchronous callback has completed, and marks the test + /// as passing if all outstanding callbacks are complete. + void markCallbackComplete() { + callbackFunctionsOutstanding--; + if (callbackFunctionsOutstanding == 0 && !isComplete) pass(); + } + + String toString() => result != null ? "$description: $result" : description; +} diff --git a/lib/src/simple_configuration.dart b/lib/src/simple_configuration.dart index bc4d7557cc54e961df627defe3ea1c22210d54c9..c95550ce22f7d8aea7b4b72d8b74573ff8ef71d7 100644 --- a/lib/src/simple_configuration.dart +++ b/lib/src/simple_configuration.dart @@ -10,13 +10,17 @@ import '../unittest.dart'; import 'configuration.dart'; import 'utils.dart'; -/// Hooks to configure the unittest library for different platforms. This class -/// implements the API in a platform-independent way. Tests that want to take -/// advantage of the platform can create a subclass and override methods from -/// this class. +/// A configuration that provides hooks to configure the unittest library for +/// different platforms. +/// +/// This class implements the [Configuration] API in a platform-independent way. +/// Tests that want to take advantage of the platform can create a subclass and +/// override methods from this class. class SimpleConfiguration extends Configuration { - // The VM won't shut down if a receive port is open. Use this to make sure - // we correctly wait for asynchronous tests. + /// A port that keeps the VM alive while we wait for asynchronous tests to + /// finish. + /// + /// The VM won't shut down as long as there's an open receive port. ReceivePort _receivePort; /// If true (the default), throw an exception at the end if any tests failed. @@ -38,16 +42,11 @@ class SimpleConfiguration extends Configuration { /// Called when each test starts. Useful to show intermediate progress on /// a test suite. Derived classes should call this first before their own /// override code. - void onTestStart(TestCase testCase) { - assert(testCase != null); - } + void onTestStart(TestCase testCase) {} - void onTestResultChanged(TestCase testCase) { - assert(testCase != null); - } - - /// Handles the logging of messages by a test case. The default in - /// this base configuration is to call print(); + /// Handles the logging of messages by a test case. + /// + /// The default in this base configuration is to call [print]. void onLogMessage(TestCase testCase, String message) { print(message); } @@ -74,17 +73,17 @@ class SimpleConfiguration extends Configuration { /// Called with the result of all test cases. /// - /// The default implementation prints the result summary using the built-in - /// [print] command. Browser tests commonly override this to reformat the - /// output. + /// The default implementation prints the result summary using [print], + /// formatted with [formatResult]. Browser tests commonly override this to + /// reformat the output. /// /// When [uncaughtError] is not null, it contains an error that occured /// outside of tests (e.g. setting up the test). void onSummary(int passed, int failed, int errors, List<TestCase> results, String uncaughtError) { // Print each test's result. - for (final t in results) { - print(formatResult(t).trim()); + for (var test in results) { + print(formatResult(test).trim()); } // Show the summary. diff --git a/lib/src/spread_args_helper.dart b/lib/src/spread_args_helper.dart deleted file mode 100644 index f93e9ba6133e4e569c9fe39f707d0ab374140fc1..0000000000000000000000000000000000000000 --- a/lib/src/spread_args_helper.dart +++ /dev/null @@ -1,163 +0,0 @@ -part of unittest; - -const _PLACE_HOLDER = const _ArgPlaceHolder(); - -/// Used to track unused positional args. -class _ArgPlaceHolder { - const _ArgPlaceHolder(); -} - -/// Simulates spread arguments using named arguments. -class _SpreadArgsHelper { - final Function callback; - final int minExpectedCalls; - final int maxExpectedCalls; - final Function isDone; - final String id; - final String reason; - int actualCalls = 0; - final TestCase testCase; - bool complete; - - _SpreadArgsHelper(Function callback, int minExpected, int maxExpected, - String id, String reason, {bool isDone()}) - : this.callback = callback, - minExpectedCalls = minExpected, - maxExpectedCalls = (maxExpected == 0 && minExpected > 0) - ? minExpected - : maxExpected, - this.isDone = isDone, - this.reason = reason == null ? '' : '\n$reason', - this.testCase = currentTestCase, - this.id = _makeCallbackId(id, callback) { - ensureInitialized(); - if (testCase == null) { - throw new StateError("No valid test. Did you forget to run your test " - "inside a call to test()?"); - } - - if (isDone != null || minExpected > 0) { - testCase._callbackFunctionsOutstanding++; - complete = false; - } else { - complete = true; - } - } - - static String _makeCallbackId(String id, Function callback) { - // Try to create a reasonable id. - if (id != null) { - return "$id "; - } else { - // If the callback is not an anonymous closure, try to get the - // name. - var fname = callback.toString(); - var prefix = "Function '"; - var pos = fname.indexOf(prefix); - if (pos > 0) { - pos += prefix.length; - var epos = fname.indexOf("'", pos); - if (epos > 0) { - return "${fname.substring(pos, epos)} "; - } - } - } - return ''; - } - - bool shouldCallBack() { - ++actualCalls; - if (testCase.isComplete) { - // Don't run if the test is done. We don't throw here as this is not - // the current test, but we do mark the old test as having an error - // if it previously passed. - if (testCase.result == PASS) { - testCase._error('Callback ${id}called ($actualCalls) after test case ' - '${testCase.description} has already been marked as ' - '${testCase.result}.$reason'); - } - return false; - } else if (maxExpectedCalls >= 0 && actualCalls > maxExpectedCalls) { - throw new TestFailure('Callback ${id}called more times than expected ' - '($maxExpectedCalls).$reason'); - } - return true; - } - - void after() { - if (!complete) { - if (minExpectedCalls > 0 && actualCalls < minExpectedCalls) return; - if (isDone != null && !isDone()) return; - - // Mark this callback as complete and remove it from the testcase - // oustanding callback count; if that hits zero the testcase is done. - complete = true; - testCase._markCallbackComplete(); - } - } - - /// Returns a function that has as many required + positional arguments as - /// [callback] (up to a total of 6). - /// - /// Optional positional arguments are supported by using const place-holders - Function get func { - if (callback is _Func6) return _max6; - if (callback is _Func5) return _max5; - if (callback is _Func4) return _max4; - if (callback is _Func3) return _max3; - if (callback is _Func2) return _max2; - if (callback is _Func1) return _max1; - if (callback is _Func0) return _max0; - - throw new ArgumentError( - 'The callback argument has more than 6 required arguments'); - } - - /// This indirection is critical. It ensures the returned function has an - /// argument count of zero. - _max0() => _max6(); - - _max1([a0 = _PLACE_HOLDER]) => _max6(a0); - - _max2([a0 = _PLACE_HOLDER, a1 = _PLACE_HOLDER]) => _max6(a0, a1); - - _max3([a0 = _PLACE_HOLDER, a1 = _PLACE_HOLDER, a2 = _PLACE_HOLDER]) => - _max6(a0, a1, a2); - - _max4([a0 = _PLACE_HOLDER, a1 = _PLACE_HOLDER, a2 = _PLACE_HOLDER, - a3 = _PLACE_HOLDER]) => _max6(a0, a1, a2, a3); - - _max5([a0 = _PLACE_HOLDER, a1 = _PLACE_HOLDER, a2 = _PLACE_HOLDER, - a3 = _PLACE_HOLDER, a4 = _PLACE_HOLDER]) => _max6(a0, a1, a2, a3, a4); - - _max6([a0 = _PLACE_HOLDER, a1 = _PLACE_HOLDER, a2 = _PLACE_HOLDER, - a3 = _PLACE_HOLDER, a4 = _PLACE_HOLDER, a5 = _PLACE_HOLDER]) { - var args = [a0, a1, a2, a3, a4, a5]; - args.removeWhere((a) => a == _PLACE_HOLDER); - - return _guardAsync(() { - if (shouldCallBack()) { - return Function.apply(callback, args); - } - }, after, testCase); - } - - _guardAsync(Function tryBody, Function finallyBody, TestCase testCase) { - assert(testCase != null); - try { - return tryBody(); - } catch (e, trace) { - _registerException(testCase, e, trace); - } finally { - if (finallyBody != null) finallyBody(); - } - } -} - -typedef _Func0(); -typedef _Func1(a); -typedef _Func2(a, b); -typedef _Func3(a, b, c); -typedef _Func4(a, b, c, d); -typedef _Func5(a, b, c, d, e); -typedef _Func6(a, b, c, d, e, f); diff --git a/lib/src/test_case.dart b/lib/src/test_case.dart index e6d2171c4c768af7991dc3963d00628387378439..37596ff599c434d206980fc2d7b0d661ace52e6b 100644 --- a/lib/src/test_case.dart +++ b/lib/src/test_case.dart @@ -2,189 +2,54 @@ // 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. -part of unittest; +library unittest.test_case; -/// Represents the state for an individual unit test. -/// -/// Create by calling [test] or [solo_test]. -class TestCase { - /// Identifier for this test. - final int id; +import '../unittest.dart'; - /// A description of what the test is specifying. - final String description; - - /// The setup function to call before the test, if any. - Function _setUp; - - /// The teardown function to call after the test, if any. - Function _tearDown; +/// An individual unit test. +abstract class TestCase { + /// A unique numeric identifier for this test case. + int get id; - /// The body of the test case. - TestFunction _testFunction; - - /// Remaining number of callbacks functions that must reach a 'done' state - /// to wait for before the test completes. - int _callbackFunctionsOutstanding = 0; + /// A description of what the test is specifying. + String get description; - String _message = ''; - /// Error or failure message. - String get message => _message; + /// The error or failure message for the tests. + /// + /// Initially an empty string. + String get message; - String _result; - /// One of [PASS], [FAIL], [ERROR], or [:null:] if the test hasn't run yet. - String get result => _result; + /// The result of the test case. + /// + /// If the test case has is completed, this will be one of [PASS], [FAIL], or + /// [ERROR]. Otherwise, it will be `null`. + String get result; /// Returns whether this test case passed. - bool get passed => _result == PASS; + bool get passed; - StackTrace _stackTrace; - /// Stack trace associated with this test, or [:null:] if it succeeded. - StackTrace get stackTrace => _stackTrace; + /// The stack trace for the error that caused this test case to fail, or + /// `null` if it succeeded. + StackTrace get stackTrace; - /// The group (or groups) under which this test is running. - final String currentGroup; + /// The name of the group within which this test is running. + String get currentGroup; - DateTime _startTime; - DateTime get startTime => _startTime; + /// The time the test case started running. + /// + /// `null` if the test hasn't yet begun running. + DateTime get startTime; - Duration _runningTime; - Duration get runningTime => _runningTime; + /// The amount of time the test case took. + /// + /// `null` if the test hasn't finished running. + Duration get runningTime; - bool _enabled = true; - - bool get enabled => _enabled; - - Completer _testComplete; - - TestCase._internal(this.id, this.description, this._testFunction) - : currentGroup = _environment.currentContext.fullName, - _setUp = _environment.currentContext.testSetup, - _tearDown = _environment.currentContext.testTeardown; + /// Whether this test is enabled. + /// + /// Disabled tests won't be run. + bool get enabled; + /// Whether this test case has finished running. bool get isComplete => !enabled || result != null; - - Function _errorHandler(String stage) => (e, stack) { - if (stack == null && e is Error) { - stack = e.stackTrace; - } - if (result == null || result == PASS) { - if (e is TestFailure) { - _fail("$e", stack); - } else { - _error("$stage failed: Caught $e", stack); - } - } - }; - - /// Perform any associated [_setUp] function and run the test. Returns - /// a [Future] that can be used to schedule the next test. If the test runs - /// to completion synchronously, or is disabled, null is returned, to - /// tell unittest to schedule the next test immediately. - Future _run() { - if (!enabled) return new Future.value(); - - _result = _stackTrace = null; - _message = ''; - - // Avoid calling [new Future] to avoid issue 11911. - return new Future.value().then((_) { - if (_setUp != null) return _setUp(); - }).catchError(_errorHandler('Setup')).then((_) { - // Skip the test if setup failed. - if (result != null) return new Future.value(); - _config.onTestStart(this); - _startTime = new DateTime.now(); - _runningTime = null; - ++_callbackFunctionsOutstanding; - var testReturn = _testFunction(); - // If _testFunction() returned a future, we want to wait for it like we - // would a callback, so if a failure occurs while waiting, we can abort. - if (testReturn is Future) { - ++_callbackFunctionsOutstanding; - testReturn - .catchError(_errorHandler('Test')) - .whenComplete(_markCallbackComplete); - } - }).catchError(_errorHandler('Test')).then((_) { - _markCallbackComplete(); - if (result == null) { - // Outstanding callbacks exist; we need to return a Future. - _testComplete = new Completer(); - return _testComplete.future.whenComplete(() { - if (_tearDown != null) { - return _tearDown(); - } - }).catchError(_errorHandler('Teardown')); - } else if (_tearDown != null) { - return _tearDown(); - } - }).catchError(_errorHandler('Teardown')).whenComplete(() { - _setUp = null; - _tearDown = null; - _testFunction = null; - }); - } - - // Set the results, notify the config, and return true if this - // is the first time the result is being set. - void _setResult(String testResult, String messageText, StackTrace stack) { - _message = messageText; - _stackTrace = getTrace(stack, formatStacks, filterStacks); - if (_stackTrace == null) _stackTrace = stack; - if (result == null) { - _result = testResult; - _config.onTestResult(this); - } else { - _result = testResult; - _config.onTestResultChanged(this); - } - } - - void _complete(String testResult, - [String messageText = '', StackTrace stack]) { - if (runningTime == null) { - // The startTime can be `null` if an error happened during setup. In this - // case we simply report a running time of 0. - if (startTime != null) { - _runningTime = new DateTime.now().difference(startTime); - } else { - _runningTime = const Duration(seconds: 0); - } - } - _setResult(testResult, messageText, stack); - if (_testComplete != null) { - var t = _testComplete; - _testComplete = null; - t.complete(this); - } - } - - void _pass() { - _complete(PASS); - } - - void _fail(String messageText, [StackTrace stack]) { - if (result != null) { - String newMessage = (result == PASS) - ? 'Test failed after initially passing: $messageText' - : 'Test failed more than once: $messageText'; - // TODO(gram): Should we combine the stack with the old one? - _complete(ERROR, newMessage, stack); - } else { - _complete(FAIL, messageText, stack); - } - } - - void _error(String messageText, [StackTrace stack]) { - _complete(ERROR, messageText, stack); - } - - void _markCallbackComplete() { - if (--_callbackFunctionsOutstanding == 0 && !isComplete) { - _pass(); - } - } - - String toString() => _result != null ? "$description: $result" : description; } diff --git a/lib/src/test_environment.dart b/lib/src/test_environment.dart index ebb39bdc5e4ab087ae00b2a3439754810e49a332..6c205a9f1c9a891b2dfeb1d4eb909d1907280a0d 100644 --- a/lib/src/test_environment.dart +++ b/lib/src/test_environment.dart @@ -2,21 +2,44 @@ // 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. -part of unittest; +library unittest.test_environment; -/// Class for encapsulating test environment state. +import 'dart:async'; + +import 'configuration.dart'; +import 'group_context.dart'; +import 'internal_test_case.dart'; + +/// The default unittest environment. +final _defaultEnvironment = new TestEnvironment(); + +/// The current unittest environment. +TestEnvironment get environment { + var environment = Zone.current[#unittest.environment]; + return environment == null ? _defaultEnvironment : environment; +} + +// The current environment's configuration. +Configuration get config => environment.config; + +/// Encapsulates the state of the test environment. /// /// This is used by the [withTestEnvironment] method to support multiple /// invocations of the unittest library within the same application /// instance. -class _TestEnvironment { +class TestEnvironment { + /// The environment's configuration. Configuration config; - // We use a 'dummy' context for the top level to eliminate null - // checks when querying the context. This allows us to easily - // support top-level [setUp]/[tearDown] functions as well. - final rootContext = new _GroupContext(); - _GroupContext currentContext; + /// The top-level group context. + /// + /// We use a 'dummy' context for the top level to eliminate null checks when + /// querying the context. This allows us to easily support top-level + /// [setUp]/[tearDown] functions as well. + final rootContext = new GroupContext.root(); + + /// The current group context. + GroupContext currentContext; /// The [currentTestCaseIndex] represents the index of the currently running /// test case. @@ -33,22 +56,19 @@ class _TestEnvironment { /// The time since we last gave asynchronous code a chance to be scheduled. int lastBreath = new DateTime.now().millisecondsSinceEpoch; - /// The set of tests to run can be restricted by using [solo_test] and - /// [solo_group]. - /// - /// As groups can be nested we use a counter to keep track of the nesting - /// level of soloing, and a flag to tell if we have seen any solo tests. + /// The number of [solo_group]s deep we are currently. int soloNestingLevel = 0; + + /// Whether we've seen a [solo_test]. bool soloTestSeen = false; /// The list of test cases to run. - final List<TestCase> testCases = new List<TestCase>(); + final testCases = new List<InternalTestCase>(); - /// The [uncaughtErrorMessage] holds the error messages that are printed - /// in the test summary. + /// The error message that is printed in the test summary. String uncaughtErrorMessage; - _TestEnvironment() { + TestEnvironment() { currentContext = rootContext; } } diff --git a/lib/unittest.dart b/lib/unittest.dart index bc1d4dc1ddab2d917cc5bf1efa6654f247c0e704..d0083270f6bfff8beadd318417477e50cb3b3a2b 100644 --- a/lib/unittest.dart +++ b/lib/unittest.dart @@ -8,8 +8,11 @@ import 'dart:async'; import 'dart:collection'; import 'src/configuration.dart'; -import 'src/expect.dart'; -import 'src/utils.dart'; +import 'src/expected_function.dart'; +import 'src/group_context.dart'; +import 'src/internal_test_case.dart'; +import 'src/test_case.dart'; +import 'src/test_environment.dart'; export 'package:matcher/matcher.dart' hide @@ -46,358 +49,339 @@ export 'src/future_matchers.dart'; export 'src/prints_matcher.dart'; export 'src/throws_matcher.dart'; export 'src/throws_matchers.dart'; +export 'src/test_case.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; +/// The signature for a function passed to [test]. +typedef dynamic TestFunction(); /// [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; + if (config == null) environment.config = new Configuration(); + return config; } +/// If `true`, stack traces are reformatted to be more readable. +bool formatStacks = true; + +/// If `true`, irrelevant frames are filtered from the stack trace. +/// +/// This does nothing if [formatStacks] is false. +bool filterStacks = true; + +/// Separator used between group names and test names. +String groupSep = ' '; + /// 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; - } + if (identical(config, value)) return; + if (config != null) { + logMessage('Warning: The unittestConfiguration has already been set. New ' + 'unittestConfiguration ignored.'); + } else { + environment.config = value; } } -/// Can be called by tests to log status. Tests should use this -/// instead of [print]. +/// Logs [message] associated with the current test case. +/// +/// Tests should use this instead of [print]. void logMessage(String message) => - _config.onLogMessage(currentTestCase, message); + config.onLogMessage(currentTestCase, message); -/// Separator used between group names and test names. -String groupSep = ' '; - -/// Tests executed in this suite. +/// The test cases that have been defined so far. List<TestCase> get testCases => - new UnmodifiableListView<TestCase>(_environment.testCases); + new UnmodifiableListView<TestCase>(environment.testCases); -/// Interval (in msecs) after which synchronous tests will insert an async -/// delay to allow DOM or other updates. +/// The interval (in milliseconds) after which a non-microtask asynchronous +/// delay will be scheduled between tests. +/// +/// This is used to avoid starving the DOM or other non-microtask events. const int BREATH_INTERVAL = 200; -/// [TestCase] currently being executed. -TestCase get currentTestCase => (_environment.currentTestCaseIndex >= 0 && - _environment.currentTestCaseIndex < testCases.length) - ? testCases[_environment.currentTestCaseIndex] +/// The [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. +/// The same as [currentTestCase], but typed as an [InternalTestCase]. +InternalTestCase get _currentTestCase => currentTestCase as InternalTestCase; + +/// The result string for a passing test case. const PASS = 'pass'; -/// Result string for a failing test case. + +/// The result string for a failing test case. const FAIL = 'fail'; -/// Result string for an test case with an error. + +/// The 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) { +/// Creates a new test case with the given description and body. +/// +/// The description will be added to the descriptions of any surrounding +/// [group]s. +void test(String description, TestFunction body) { _requireNotRunning(); ensureInitialized(); - if (!_environment.soloTestSeen || _environment.soloNestingLevel > 0) { - var testcase = - new TestCase._internal(testCases.length + 1, _fullSpec(spec), body); - _testCases.add(testcase); - } + + if (environment.soloTestSeen && environment.soloNestingLevel == 0) return; + var testCase = new InternalTestCase( + testCases.length + 1, _fullDescription(description), body); + environment.testCases.add(testCase); } -/// Convenience function for skipping a test. +/// Returns [description] with all of its group prefixes prepended. +String _fullDescription(String description) { + var group = environment.currentContext.fullName; + if (description == null) return group; + return group != '' ? '$group$groupSep$description' : description; +} + +/// A 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. +/// Creates a new test case with the given description and body. /// -/// [skip_test] and [skip_group] take precedence over soloing, by virtue of the -/// fact that they are effectively no-ops. +/// If [solo_test] is used instead of [test], then all non-solo tests will be +/// disabled. Note that if [solo_group] is used as well, 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] versus [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. void solo_test(String spec, TestFunction body) { _requireNotRunning(); ensureInitialized(); - if (!_environment.soloTestSeen) { - _environment.soloTestSeen = true; + if (!environment.soloTestSeen) { + environment.soloTestSeen = true; // This is the first solo-ed test. Discard all tests up to now. - _testCases.clear(); + environment.testCases.clear(); } - ++_environment.soloNestingLevel; + environment.soloNestingLevel++; try { test(spec, body); } finally { - --_environment.soloNestingLevel; + environment.soloNestingLevel--; } } -/// Indicate that [callback] is expected to be called a [count] number of times +/// Indicate that [callback] is expected to be called [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. +/// The unittest framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. Using [expectAsync] +/// will also ensure that errors that occur within [callback] are tracked and +/// reported. [callback] may take up to six optional or required positional +/// arguments; named arguments are not supported. /// -/// [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. +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. /// -/// [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. +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. Function expectAsync(Function callback, {int count: 1, int max: 0, String id, String reason}) => - new _SpreadArgsHelper(callback, count, max, id, reason).func; + new ExpectedFunction(callback, count, max, id: id, reason: 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). +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. Using [expectAsyncUntil] will +/// also ensure that errors that occur within [callback] are tracked and +/// reported. [callback] may take up to six optional or required positional +/// arguments; named arguments are not supported. /// -/// [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. +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. Function expectAsyncUntil(Function callback, bool isDone(), - {String id, String reason}) => - new _SpreadArgsHelper(callback, 0, -1, id, reason, isDone: isDone).func; + {String id, String reason}) => new ExpectedFunction(callback, 0, -1, + id: id, reason: reason, isDone: isDone).func; -/// Creates a new named group of tests. +/// Creates a 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. +/// A group's description is included in the descriptions of any tests or +/// sub-groups it contains. [setUp] and [tearDown] are also scoped to the +/// containing group. void group(String description, void body()) { ensureInitialized(); _requireNotRunning(); - _environment.currentContext = - new _GroupContext(_environment.currentContext, description); + environment.currentContext = + new GroupContext(environment.currentContext, description); try { body(); } catch (e, trace) { var stack = (trace == null) ? '' : ': ${trace.toString()}'; - _environment.uncaughtErrorMessage = "${e.toString()}$stack"; + environment.uncaughtErrorMessage = "${e.toString()}$stack"; } finally { // Now that the group is over, restore the previous one. - _environment.currentContext = _environment.currentContext.parent; + environment.currentContext = environment.currentContext.parent; } } -/// Like [skip_test], but for groups. +/// A convenience function for skipping a group of tests. void skip_group(String description, void body()) {} -/// Like [solo_test], but for groups. +/// Creates a group of tests. +/// +/// If [solo_group] is used instead of [group], then all tests not declared with +/// [solo_test] or in a [solo_group] will be disabled. Note that all tests in a +/// [solo_group] will be run, regardless of whether they're declared with [test] +/// or [solo_test]. +/// +/// [skip_test] and [skip_group] take precedence over [solo_group]. void solo_group(String description, void body()) { _requireNotRunning(); ensureInitialized(); - if (!_environment.soloTestSeen) { - _environment.soloTestSeen = true; + if (!environment.soloTestSeen) { + environment.soloTestSeen = true; // This is the first solo-ed group. Discard all tests up to now. - _testCases.clear(); + environment.testCases.clear(); } - ++_environment.soloNestingLevel; + ++environment.soloNestingLevel; try { group(description, body); } finally { - --_environment.soloNestingLevel; + --environment.soloNestingLevel; } } -/// Register a [setUp] function for a test [group]. +/// Registers a function to be run before tests. /// -/// 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) { +/// This function will be called before each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run after any set-up callbacks in parent groups or +/// at the top level. +void setUp(Function callback) { _requireNotRunning(); - _environment.currentContext.testSetup = setupTest; + environment.currentContext.testSetUp = callback; } -/// Register a [tearDown] function for a test [group]. +/// Registers a function to be run after tests. /// -/// This function will be called after each test in the group is run. +/// This function will be called after each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. /// -/// 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) { +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run before any tear-down callbacks in parent groups or +/// at the top level. +void tearDown(Function callback) { _requireNotRunning(); - _environment.currentContext.testTeardown = teardownTest; + environment.currentContext.testTearDown = callback; } /// Advance to the next test case. void _nextTestCase() { - _environment.currentTestCaseIndex++; + 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]) { +/// Handle an error that occurs outside of any test. +void handleExternalError(e, String message, [stackTrace]) { var msg = '$message\nCaught $e'; if (currentTestCase != null) { - currentTestCase._error(msg, stack); + _currentTestCase.error(msg, stackTrace); } else { - _environment.uncaughtErrorMessage = "$msg: $stack"; + environment.uncaughtErrorMessage = "$msg: $stackTrace"; } } -/// Filter the tests by [testFilter]. +/// Remove any tests that match [testFilter]. +/// +/// [testFilter] can be a predicate function, a [RegExp], or a [String]. If it's +/// a function, it's called with each [TestCase]. If it's a [String], it's +/// parsed as a [RegExp] and matched against each [TestCase.description]. /// -/// [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. +/// 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); + var 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); + environment.testCases.retainWhere(filterFunction); } /// Runs all queued tests, one at a time. void runTests() { _requireNotRunning(); _ensureInitialized(false); - _environment.currentTestCaseIndex = 0; - _config.onStart(); + 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); - } -} +/// Registers an exception that was caught for the current test. +void registerException(error, [StackTrace stackTrace]) => + _currentTestCase.registerException(error, stackTrace); /// Runs the next test. void _runTest() { - if (_environment.currentTestCaseIndex >= testCases.length) { - assert(_environment.currentTestCaseIndex == testCases.length); + 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. - } + return; + } + + var testCase = _currentTestCase; + var 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 + testCase.registerException(error, stack); + }); + + var timer; + var timeout = unittestConfiguration.timeout; + 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. - } - }); } + + 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. +/// Notify the configuration that the testing has finished. void _completeTests() { - if (!_environment.initialized) return; - int passed = 0; - int failed = 0; - int errors = 0; + if (!environment.initialized) return; - for (TestCase t in testCases) { - switch (t.result) { + var passed = 0; + var failed = 0; + var errors = 0; + for (var testCase in testCases) { + switch (testCase.result) { case PASS: passed++; break; @@ -409,97 +393,77 @@ void _completeTests() { break; } } - _config.onSummary( - passed, failed, errors, testCases, _environment.uncaughtErrorMessage); - _config.onDone(passed > 0 && + + 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; + environment.uncaughtErrorMessage == null); + environment.initialized = false; + environment.currentTestCaseIndex = -1; } -/// Lazily initializes the test library if not already initialized. +/// Initializes the test environment if it hasn't already been initialized. void ensureInitialized() { _ensureInitialized(true); } +/// Initializes the test environment. +/// +/// If [configAutoStart] is `true`, schedule a microtask to run the tests. This +/// microtask is expected to run after all the tests are defined. void _ensureInitialized(bool configAutoStart) { - if (_environment.initialized) { - return; - } - _environment.initialized = true; + if (environment.initialized) return; + + environment.initialized = true; - _environment.uncaughtErrorMessage = null; + 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); - } + // Immediately queue the suite up. It will run after a timeout (i.e. after + // main() has returned). + if (configAutoStart && config.autoStart) scheduleMicrotask(runTests); } -/// Select a solo test by ID. -void setSoloTest(int id) => _testCases.retainWhere((t) => t.id == id); +/// Remove all tests other than the one identified by [id]. +void setSoloTest(int id) => + environment.testCases.retainWhere((t) => t.id == id); + +/// Enable the test identified by [id]. +void enableTest(int id) => _setTestEnabledState(id, enable: true); -/// Enable/disable a test by ID. -void _setTestEnabledState(int testId, bool state) { +/// Disable the test by [id]. +void disableTest(int id) => _setTestEnabledState(id, enable: false); + +/// Enable or disable the test identified by [id]. +void _setTestEnabledState(int id, {bool enable: true}) { // Try fast path first. - if (testCases.length > testId && testCases[testId].id == testId) { - testCases[testId]._enabled = state; + if (testCases.length > id && testCases[id].id == id) { + environment.testCases[id].enabled = enable; } else { for (var i = 0; i < testCases.length; i++) { - if (testCases[i].id == testId) { - testCases[i]._enabled = state; - break; - } + if (testCases[i].id != id) continue; + environment.testCases[i].enabled = enable; + 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; - +/// Throws a [StateError] if tests are running. void _requireNotRunning() { - if (_environment.currentTestCaseIndex != -1) { - throw new StateError('Not allowed when tests are running.'); - } + if (environment.currentTestCaseIndex == -1) return; + throw new StateError('Not allowed when tests are running.'); } -/// Method to create a test environment running in its own zone scope. +/// Creates 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()) { +/// 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 tests. +withTestEnvironment(callback()) { return runZoned(callback, - zoneValues: {_UNITTEST_ENVIRONMENT: new _TestEnvironment()}); + zoneValues: {#unittest.environment: new TestEnvironment()}); } diff --git a/test/late_exception_test.dart b/test/late_exception_test.dart index 14d14fe742e2492a3023526bae18fedc05241ea3..94ad30b3fdd3f832c322b45ff410c0e9b43b27d3 100644 --- a/test/late_exception_test.dart +++ b/test/late_exception_test.dart @@ -28,7 +28,7 @@ void _test(message) { }, [ { 'description': 'testOne', - 'message': 'Callback called (2) after test case testOne has already been ' + 'message': 'Callback called (2) after test case testOne had already been ' 'marked as pass.', 'result': 'error', },