blob: a0151c385ec722bcb38e6d045e5f747d9111a1ba [file] [log] [blame]
// 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 test.backend.invoker;
import 'dart:async';
import 'package:stack_trace/stack_trace.dart';
import '../frontend/expect.dart';
import '../utils.dart';
import 'closed_exception.dart';
import 'live_test.dart';
import 'live_test_controller.dart';
import 'metadata.dart';
import 'outstanding_callback_counter.dart';
import 'state.dart';
import 'suite.dart';
import 'test.dart';
/// A test in this isolate.
class LocalTest implements Test {
final String name;
final Metadata metadata;
/// The test body.
final AsyncFunction _body;
/// The callback used to clean up after the test.
///
/// This is separated out from [_body] because it needs to run once the test's
/// asynchronous computation has finished, even if that's different from the
/// completion of the main body of the test.
final AsyncFunction _tearDown;
LocalTest(this.name, this.metadata, body(), {tearDown()})
: _body = body,
_tearDown = tearDown;
/// Loads a single runnable instance of this test.
LiveTest load(Suite suite) {
var invoker = new Invoker._(suite, this);
return invoker.liveTest;
}
Test change({String name, Metadata metadata}) {
if (name == null) name = this.name;
if (metadata == null) metadata = this.metadata;
return new LocalTest(name, metadata, _body, tearDown: _tearDown);
}
}
/// The class responsible for managing the lifecycle of a single local test.
///
/// The current invoker is accessible within the zone scope of the running test
/// using [Invoker.current]. It's used to track asynchronous callbacks and
/// report asynchronous errors.
class Invoker {
/// The live test being driven by the invoker.
///
/// This provides a view into the state of the test being executed.
LiveTest get liveTest => _controller.liveTest;
LiveTestController _controller;
/// Whether the test has been closed.
///
/// Once the test is closed, [expect] and [expectAsync] will throw
/// [ClosedException]s whenever accessed to help the test stop executing as
/// soon as possible.
bool get closed => _closed;
bool _closed = false;
/// The test being run.
LocalTest get _test => liveTest.test as LocalTest;
/// The test metadata merged with the suite metadata.
final Metadata metadata;
/// The outstanding callback counter for the current zone.
OutstandingCallbackCounter get _outstandingCallbacks {
var counter = Zone.current[this];
if (counter != null) return counter;
throw new StateError("Can't add or remove outstanding callbacks outside "
"of a test body.");
}
/// The current invoker, or `null` if none is defined.
///
/// An invoker is only set within the zone scope of a running test.
static Invoker get current {
// TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
return Zone.current[#test.invoker];
}
Invoker._(Suite suite, LocalTest test)
: metadata = suite.metadata.merge(test.metadata) {
_controller = new LiveTestController(suite, test, _onRun, () {
_closed = true;
});
}
/// Tells the invoker that there's a callback running that it should wait for
/// before considering the test successful.
///
/// Each call to [addOutstandingCallback] should be followed by a call to
/// [removeOutstandingCallback] once the callbak is no longer running. Note
/// that only successful tests wait for outstanding callbacks; as soon as a
/// test experiences an error, any further calls to [addOutstandingCallback]
/// or [removeOutstandingCallback] will do nothing.
///
/// Throws a [ClosedException] if this test has been closed.
void addOutstandingCallback() {
if (closed) throw new ClosedException();
_outstandingCallbacks.addOutstandingCallback();
}
/// Tells the invoker that a callback declared with [addOutstandingCallback]
/// is no longer running.
void removeOutstandingCallback() =>
_outstandingCallbacks.removeOutstandingCallback();
/// Notifies the invoker of an asynchronous error.
///
/// Note that calling this explicitly is rarely necessary, since any
/// otherwise-uncaught errors will be forwarded to the invoker anyway.
void handleError(error, [StackTrace stackTrace]) {
if (stackTrace == null) stackTrace = new Chain.current();
var afterSuccess = liveTest.isComplete &&
liveTest.state.result == Result.success;
if (error is! TestFailure) {
_controller.setState(const State(Status.complete, Result.error));
} else if (liveTest.state.result != Result.error) {
_controller.setState(const State(Status.complete, Result.failure));
}
_controller.addError(error, stackTrace);
_outstandingCallbacks.removeAllOutstandingCallbacks();
// If a test was marked as success but then had an error, that indicates
// that it was poorly-written and could be flaky.
if (!afterSuccess) return;
handleError(
"This test failed after it had already completed. Make sure to use "
"[expectAsync]\n"
"or the [completes] matcher when testing async code.",
stackTrace);
}
/// The method that's run when the test is started.
void _onRun() {
_controller.setState(const State(Status.running, Result.success));
var outstandingCallbacksForBody = new OutstandingCallbackCounter();
Chain.capture(() {
runZonedWithValues(() {
// TODO(nweiz): Reset this timer whenever the user's code interacts
// with the library.
var timeout = metadata.timeout.apply(new Duration(seconds: 30));
var timer = new Timer(timeout, () {
if (liveTest.isComplete) return;
handleError(
new TimeoutException(
"Test timed out after ${niceDuration(timeout)}.", timeout));
});
// Run the test asynchronously so that the "running" state change has
// a chance to hit its event handler(s) before the test produces an
// error. If an error is emitted before the first state change is
// handled, we can end up with [onError] callbacks firing before the
// corresponding [onStateChange], which violates the timing
// guarantees.
new Future(_test._body)
.then((_) => removeOutstandingCallback());
_outstandingCallbacks.noOutstandingCallbacks.then((_) {
if (_test._tearDown == null) return null;
// Reset the outstanding callback counter to wait for callbacks from
// the test's `tearDown` to complete.
var outstandingCallbacksForTearDown = new OutstandingCallbackCounter();
runZonedWithValues(() {
new Future.sync(_test._tearDown)
.then((_) => removeOutstandingCallback());
}, onError: handleError, zoneValues: {
this: outstandingCallbacksForTearDown
});
return outstandingCallbacksForTearDown.noOutstandingCallbacks;
}).then((_) {
timer.cancel();
_controller.setState(
new State(Status.complete, liveTest.state.result));
// Use [Timer.run] here to avoid starving the DOM or other
// non-microtask events.
Timer.run(_controller.completer.complete);
});
}, zoneValues: {
#test.invoker: this,
// Use the invoker as a key so that multiple invokers can have different
// outstanding callback counters at once.
this: outstandingCallbacksForBody
},
zoneSpecification: new ZoneSpecification(
print: (self, parent, zone, line) => _controller.print(line)),
onError: handleError);
});
}
}