blob: 1b166d30166fd5618cf1daf60f55842a36047a0a [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 'package:async/async.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
import 'environment.dart';
import 'suite.dart';
/// A suite produced and consumed by the test runner that has runner-specific
/// logic and lifecycle management.
///
/// This is separated from [Suite] because the backend library (which will
/// eventually become its own package) is primarily for test code itself to use,
/// for which the [RunnerSuite] APIs don't make sense.
///
/// A [RunnerSuite] can be produced and controlled using a
/// [RunnerSuiteController].
class RunnerSuite extends Suite {
final RunnerSuiteController _controller;
/// The environment in which this suite runs.
Environment get environment => _controller._environment;
/// The configuration for this suite.
SuiteConfiguration get config => _controller._config;
/// Whether the suite is paused for debugging.
///
/// When using a dev inspector, this may also mean that the entire browser is
/// paused.
bool get isDebugging => _controller._isDebugging;
/// A broadcast stream that emits an event whenever the suite is paused for
/// debugging or resumed afterwards.
///
/// The event is `true` when debugging starts and `false` when it ends.
Stream<bool> get onDebugging => _controller._onDebuggingController.stream;
/// Returns a channel that communicates with the remote suite.
///
/// This connects to a channel created by code in the test worker calling
/// `suiteChannel()` from `remote_platform_helpers.dart` with the same name.
/// It can be used used to send and receive any JSON-serializable object.
StreamChannel channel(String name) => _controller.channel(name);
/// A shortcut constructor for creating a [RunnerSuite] that never goes into
/// debugging mode and doesn't support suite channels.
factory RunnerSuite(Environment environment, SuiteConfiguration config,
Group group, SuitePlatform platform,
{String path, Function() onClose}) {
var controller =
RunnerSuiteController._local(environment, config, onClose: onClose);
var suite = RunnerSuite._(controller, group, path, platform);
controller._suite = Future.value(suite);
return suite;
}
RunnerSuite._(
this._controller, Group group, String path, SuitePlatform platform)
: super(group, platform, path: path);
@override
RunnerSuite filter(bool Function(Test) callback) {
var filtered = group.filter(callback);
filtered ??= Group.root([], metadata: metadata);
return RunnerSuite._(_controller, filtered, path, platform);
}
/// Closes the suite and releases any resources associated with it.
Future close() => _controller._close();
/// Collects a hit-map containing merged coverage.
///
/// Result is suitable for input to the coverage formatters provided by
/// `package:coverage`.
Future<Map<String, dynamic>> gatherCoverage() async =>
(await _controller._gatherCoverage?.call()) ?? {};
}
/// A class that exposes and controls a [RunnerSuite].
class RunnerSuiteController {
/// The suite controlled by this controller.
Future<RunnerSuite> get suite => _suite;
Future<RunnerSuite> _suite;
/// The backing value for [suite.environment].
final Environment _environment;
/// The configuration for this suite.
final SuiteConfiguration _config;
/// A channel that communicates with the remote suite.
final MultiChannel _suiteChannel;
/// The function to call when the suite is closed.
final Function() _onClose;
/// The backing value for [suite.isDebugging].
bool _isDebugging = false;
/// The controller for [suite.onDebugging].
final _onDebuggingController = StreamController<bool>.broadcast();
/// The channel names that have already been used.
final _channelNames = <String>{};
/// Collects a hit-map containing merged coverage.
final Future<Map<String, dynamic>> Function() _gatherCoverage;
RunnerSuiteController(this._environment, this._config, this._suiteChannel,
Future<Group> groupFuture, SuitePlatform platform,
{String path,
Function() onClose,
Future<Map<String, dynamic>> Function() gatherCoverage})
: _onClose = onClose,
_gatherCoverage = gatherCoverage {
_suite =
groupFuture.then((group) => RunnerSuite._(this, group, path, platform));
}
/// Used by [new RunnerSuite] to create a runner suite that's not loaded from
/// an external source.
RunnerSuiteController._local(this._environment, this._config,
{Function() onClose,
Future<Map<String, dynamic>> Function() gatherCoverage})
: _suiteChannel = null,
_onClose = onClose,
_gatherCoverage = gatherCoverage;
/// Sets whether the suite is paused for debugging.
///
/// If this is different than [suite.isDebugging], this will automatically
/// send out an event along [suite.onDebugging].
void setDebugging(bool debugging) {
if (debugging == _isDebugging) return;
_isDebugging = debugging;
_onDebuggingController.add(debugging);
}
/// Returns a channel that communicates with the remote suite.
///
/// This connects to a channel created by code in the test worker calling
/// `suiteChannel()` from `remote_platform_helpers.dart` with the same name.
/// It can be used used to send and receive any JSON-serializable object.
///
/// This is exposed on the [RunnerSuiteController] so that runner plugins can
/// communicate with the workers they spawn before the associated [suite] is
/// fully loaded.
StreamChannel channel(String name) {
if (!_channelNames.add(name)) {
throw StateError('Duplicate RunnerSuite.channel() connection "$name".');
}
var channel = _suiteChannel.virtualChannel();
_suiteChannel.sink
.add({'type': 'suiteChannel', 'name': name, 'id': channel.id});
return channel;
}
/// The backing function for [suite.close].
Future _close() => _closeMemo.runOnce(() async {
await _onDebuggingController.close();
if (_onClose != null) await _onClose();
});
final _closeMemo = AsyncMemoizer();
}