blob: b0a6154ecde88203bce3ba2596e30cb92b6ab93d [file] [log] [blame]
// Copyright (c) 2023, 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 'src/backend/group.dart';
import 'src/backend/invoker.dart';
import 'src/backend/live_test.dart';
import 'src/backend/metadata.dart';
import 'src/backend/runtime.dart';
import 'src/backend/state.dart';
import 'src/backend/suite.dart';
import 'src/backend/suite_platform.dart';
@Deprecated('These classes are unused for monitoring tests. Use `State`.')
export 'src/backend/state.dart' show Result, Status;
export 'src/backend/test_failure.dart' show TestFailure;
/// A monitor for the behavior of a callback when it is run as the body of a
/// test case.
///
/// Allows running a callback as the body of a local test case and querying for
/// the current [state], and [errors], and subscribing to future errors.
///
/// Use [run] to run a test body and query for the success or failure.
///
/// Use [start] to start a test and query for whether it has finished running.
final class TestCaseMonitor {
final LiveTest _liveTest;
final _done = Completer<void>();
TestCaseMonitor._(FutureOr<void> Function() body)
: _liveTest = _createTest(body);
/// Run [body] as a test case and return a [TestCaseMonitor] with the result.
///
/// The [state] will either [State.passed], [State.skipped], or
/// [State.failed], the test will no longer be running.
///
/// {@template result-late-fail}
/// Note that a test can change state from [State.passed] to [State.failed]
/// if the test surfaces an unawaited asynchronous error.
/// {@endtemplate}
///
/// ```dart
/// final monitor = await TestCaseMonitor.run(() {
/// fail('oh no!');
/// });
/// assert(monitor.state == State.failed);
/// assert((monitor.errors.single.error as TestFailure).message == 'oh no!');
/// ```
static Future<TestCaseMonitor> run(FutureOr<void> Function() body) async {
final monitor = TestCaseMonitor.start(body);
await monitor.onDone;
return monitor;
}
/// Start [body] as a test case and return a [TestCaseMonitor] with the status
/// and result.
///
/// The [state] will start as [State.pending] if queried synchronously, but it
/// will switch to [State.running]. After `onDone` completes the state will be
/// one of [State.passed], [State.skipped], or [State.failed].
///
/// {@macro result-late-fail}
///
/// ```dart
/// late void Function() completeWork;
/// final monitor = TestCaseMonitor.start(() {
/// final outstandingWork = TestHandle.current.markPending();
/// completeWork = outstandingWork.complete;
/// });
/// await pumpEventQueue();
/// assert(monitor.state == State.running);
/// completeWork();
/// await monitor.onDone;
/// assert(monitor.state == State.passed);
/// ```
static TestCaseMonitor start(FutureOr<void> Function() body) =>
TestCaseMonitor._(body).._start();
void _start() {
_liveTest.run().whenComplete(_done.complete);
}
/// A future that completes after this test has finished running, or has
/// surfaced an error.
Future<void> get onDone => _done.future;
/// The running and success or failure status for the test case.
State get state {
final status = _liveTest.state.status;
if (status == Status.pending) return State.pending;
if (status == Status.running) return State.running;
final result = _liveTest.state.result;
if (result == Result.skipped) return State.skipped;
if (result == Result.success) return State.passed;
// result == Result.failure || result == Result.error
return State.failed;
}
/// The errors surfaced by the test.
///
/// A test with any errors will have a [state] of [State.failed].
///
/// {@macro result-late-fail}
///
/// A test may have more than one error if there were unhandled asynchronous
/// errors surfaced after the test is done.
Iterable<AsyncError> get errors => _liveTest.errors;
/// A stream of errors surfaced by the test.
///
/// This stream will not close, asynchronous errors may be surfaced within the
/// test's error zone at any point.
Stream<AsyncError> get onError => _liveTest.onError;
}
/// Returns a local [LiveTest] that runs [body].
LiveTest _createTest(FutureOr<void> Function() body) {
var test = LocalTest('test', Metadata(chainStackTraces: true), body);
var suite = Suite(Group.root([test]), _suitePlatform, ignoreTimeouts: false);
return test.load(suite);
}
/// A dummy suite platform to use for testing suites.
final _suitePlatform =
SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler);
/// The running and success state of a test monitored by a [TestCaseMonitor].
enum State {
/// The test is has not yet started.
pending,
/// The test is running and has not yet failed.
running,
/// The test has completed without any error.
///
/// This implies that the test body has completed, and no error has surfaced
/// *yet*. However, it this doesn't mean that the test won't fail in the
/// future.
passed,
/// The test, or some part of it, has been skipped.
///
/// This does not imply that the test has not had an error, but if there are
/// errors they are ignored.
skipped,
/// The test has failed.
///
/// An test fails when any exception, typically a [TestFailure], is thrown in
/// the test's zone. A test that has failed may still have additional errors
/// that surface as unhandled asynchronous errors.
failed,
}