blob: 0aad846c6d6d0d1c1e75702b8e01bf2f46263728 [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.
import 'dart:async';
import 'dart:collection';
import 'package:stack_trace/stack_trace.dart';
import 'group.dart';
import 'live_test.dart';
import 'message.dart';
import 'state.dart';
import 'suite.dart';
import 'test.dart';
/// An implementation of [LiveTest] that's controlled by a [LiveTestController].
class _LiveTest extends LiveTest {
final LiveTestController _controller;
@override
Suite get suite => _controller._suite;
@override
List<Group> get groups => _controller._groups;
@override
Test get test => _controller._test;
@override
State get state => _controller._state;
@override
Stream<State> get onStateChange =>
_controller._onStateChangeController.stream;
@override
List<AsyncError> get errors => UnmodifiableListView(_controller._errors);
@override
Stream<AsyncError> get onError => _controller._onErrorController.stream;
@override
Stream<Message> get onMessage => _controller._onMessageController.stream;
@override
Future<void> get onComplete => _controller.onComplete;
@override
Future<void> run() => _controller._run();
@override
Future<void> close() => _controller._close();
_LiveTest(this._controller);
}
/// A controller that drives a [LiveTest].
///
/// This is a utility class to make it easier for implementors of [Test] to
/// create the [LiveTest] returned by [Test.load]. The [LiveTest] is accessible
/// through [LiveTestController.liveTest].
///
/// This automatically handles some of [LiveTest]'s guarantees, but for the most
/// part it's the caller's responsibility to make sure everything gets
/// dispatched in the correct order.
class LiveTestController {
/// The [LiveTest] controlled by [this].
LiveTest get liveTest => _liveTest;
LiveTest _liveTest;
/// The test suite that's running [this].
final Suite _suite;
/// The groups containing [this].
final List<Group> _groups;
/// The test that's being run.
final Test _test;
/// The function that will actually start the test running.
final Function _onRun;
/// A function to run when the test is closed.
///
/// This may be `null`.
final Function _onClose;
/// The list of errors caught by the test.
final _errors = <AsyncError>[];
/// The current state of the test.
var _state = const State(Status.pending, Result.success);
/// The controller for [LiveTest.onStateChange].
///
/// This is synchronous to ensure that events are well-ordered across multiple
/// streams.
final _onStateChangeController =
StreamController<State>.broadcast(sync: true);
/// The controller for [LiveTest.onError].
///
/// This is synchronous to ensure that events are well-ordered across multiple
/// streams.
final _onErrorController = StreamController<AsyncError>.broadcast(sync: true);
/// The controller for [LiveTest.onMessage].
///
/// This is synchronous to ensure that events are well-ordered across multiple
/// streams.
final _onMessageController = StreamController<Message>.broadcast(sync: true);
/// The completer for [LiveTest.onComplete];
final completer = Completer<void>();
/// Whether [run] has been called.
var _runCalled = false;
/// Whether [close] has been called.
bool get _isClosed => _onErrorController.isClosed;
/// Creates a new controller for a [LiveTest].
///
/// [test] is the test being run; [suite] is the suite that contains it.
///
/// [onRun] is a function that's called from [LiveTest.run]. It should start
/// the test running. The controller takes care of ensuring that
/// [LiveTest.run] isn't called more than once and that [LiveTest.onComplete]
/// is returned.
///
/// [onClose] is a function that's called the first time [LiveTest.close] is
/// called. It should clean up any resources that have been allocated for the
/// test and ensure that the test finishes quickly if it's still running. It
/// will only be called if [onRun] has been called first.
///
/// If [groups] is passed, it's used to populate the list of groups that
/// contain this test. Otherwise, `suite.group` is used.
LiveTestController(
Suite suite, this._test, void Function() onRun, void Function() onClose,
{Iterable<Group> groups})
: _suite = suite,
_onRun = onRun,
_onClose = onClose,
_groups = groups == null ? [suite.group] : List.unmodifiable(groups) {
_liveTest = _LiveTest(this);
}
/// Adds an error to the [LiveTest].
///
/// This both adds the error to [LiveTest.errors] and emits it via
/// [LiveTest.onError]. [stackTrace] is automatically converted into a [Chain]
/// if it's not one already.
void addError(error, StackTrace stackTrace) {
if (_isClosed) return;
var asyncError = AsyncError(error, Chain.forTrace(stackTrace));
_errors.add(asyncError);
_onErrorController.add(asyncError);
}
/// Sets the current state of the [LiveTest] to [newState].
///
/// If [newState] is different than the old state, this both sets
/// [LiveTest.state] and emits the new state via [LiveTest.onStateChanged]. If
/// it's not different, this does nothing.
void setState(State newState) {
if (_isClosed) return;
if (_state == newState) return;
_state = newState;
_onStateChangeController.add(newState);
}
/// Emits message over [LiveTest.onMessage].
void message(Message message) {
if (_onMessageController.hasListener) {
_onMessageController.add(message);
} else {
// Make sure all messages get surfaced one way or another to aid in
// debugging.
Zone.root.print(message.text);
}
}
/// A wrapper for [_onRun] that ensures that it follows the guarantees for
/// [LiveTest.run].
Future<void> _run() {
if (_runCalled) {
throw StateError('LiveTest.run() may not be called more than once.');
} else if (_isClosed) {
throw StateError('LiveTest.run() may not be called for a closed '
'test.');
}
_runCalled = true;
_onRun();
return liveTest.onComplete;
}
/// Returns a future that completes when the test is complete.
///
/// We also wait for the state to transition to Status.complete.
Future<void> get onComplete => completer.future;
/// A wrapper for [_onClose] that ensures that all controllers are closed.
Future<void> _close() {
if (_isClosed) return onComplete;
_onStateChangeController.close();
_onErrorController.close();
if (_runCalled) {
_onClose();
} else {
completer.complete();
}
return onComplete;
}
}