blob: c4f7612c64e3c65dba1ab66dc5bc5e7b068b420d [file] [log] [blame]
// Copyright (c) 2020, 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.
//
// @dart=2.9
import 'dart:async';
import 'dart:collection';
import 'package:path/path.dart' as p;
import 'package:test_api/backend.dart'; //ignore: deprecated_member_use
import 'package:test_api/src/backend/declarer.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/group.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/group_entry.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/test.dart'; //ignore: implementation_imports
import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
import 'runner/configuration.dart';
import 'runner/engine.dart';
import 'runner/plugin/environment.dart';
import 'runner/reporter.dart';
import 'runner/reporter/expanded.dart';
import 'runner/runner_suite.dart';
import 'runner/suite.dart';
import 'util/print_sink.dart';
/// Runs all unskipped test cases declared in [testMain].
///
/// Test suite level metadata defined in annotations is not read. No filtering
/// is applied except for the filtering defined by `solo` or `skip` arguments to
/// `group` and `test`. Returns [true] if all tests passed.
Future<bool> directRunTests(FutureOr<void> Function() testMain,
{Reporter Function(Engine) /*?*/ reporterFactory}) =>
_directRunTests(testMain, reporterFactory: reporterFactory);
/// Runs a single test declared in [testMain] matched by it's full test name.
///
/// There must be exactly one test defined with the name [fullTestName]. Note
/// that not all tests and groups are checked, so a test case that is not be
/// intended to be run (due to a `solo` on a different test) may still be run
/// with this API. Only the test names returned by [enumerateTestCases] should
/// be used to prevent running skipped tests.
///
/// Return [true] if the test passes.
///
/// If there are no tests matching [fullTestName] a [MissingTestException] is
/// thrown. If there is more than one test with the name [fullTestName] they
/// will both be run, then a [DuplicateTestnameException] will be thrown.
Future<bool> directRunSingleTest(
FutureOr<void> Function() testMain, String fullTestName,
{Reporter Function(Engine) /*?*/ reporterFactory}) =>
_directRunTests(testMain,
reporterFactory: reporterFactory, fullTestName: fullTestName);
Future<bool> _directRunTests(FutureOr<void> Function() testMain,
{Reporter Function(Engine) /*?*/ reporterFactory,
String /*?*/ fullTestName}) async {
reporterFactory ??= (engine) => ExpandedReporter.watch(engine, PrintSink(),
color: Configuration.empty.color, printPath: false, printPlatform: false);
final declarer = Declarer(fullTestName: fullTestName);
await declarer.declare(testMain);
final suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
declarer.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
path: p.prettyUri(Uri.base));
final engine = Engine()
..suiteSink.add(suite)
..suiteSink.close();
reporterFactory(engine);
final success = await runZoned(() => Invoker.guard(engine.run),
zoneValues: {#test.declarer: declarer});
if (fullTestName != null) {
final testCount = engine.liveTests.length;
if (testCount > 1) {
throw DuplicateTestNameException(fullTestName);
}
if (testCount == 0) {
throw MissingTestException(fullTestName);
}
}
return success;
}
/// Runs [testMain] and returns the names of all declared tests.
///
/// Test names declared must be unique. If any test repeats the full name,
/// including group prefixes, of a prior test a [DuplicateTestNameException]
/// will be thrown.
///
/// Skipped tests are ignored.
Future<Set<String>> enumerateTestCases(
FutureOr<void> Function() testMain) async {
final declarer = Declarer();
await declarer.declare(testMain);
final toVisit = Queue<GroupEntry>.of([declarer.build()]);
final allTestNames = <String>{};
final unskippedTestNames = <String>{};
while (toVisit.isNotEmpty) {
final current = toVisit.removeLast();
if (current is Group) {
toVisit.addAll(current.entries.reversed);
} else if (current is Test) {
if (!allTestNames.add(current.name)) {
throw DuplicateTestNameException(current.name);
}
if (current.metadata.skip) continue;
unskippedTestNames.add(current.name);
} else {
throw StateError('Unandled Group Entry: ${current.runtimeType}');
}
}
return unskippedTestNames;
}
/// An exception thrown when two test cases in the same test suite (same `main`)
/// have an identical name.
class DuplicateTestNameException implements Exception {
final String name;
DuplicateTestNameException(this.name);
@override
String toString() => 'A test with the name "$name" was already declared. '
'Test cases must have unique names.';
}
/// An exception thrown when a specific test was requested by name that does not
/// exist.
class MissingTestException implements Exception {
final String name;
MissingTestException(this.name);
@override
String toString() =>
'A test with the name "$name" was not declared in the test suite.';
}