Skip to content

Commit

Permalink
Add --fail-fast flag (#2040)
Browse files Browse the repository at this point in the history
Closes #1841

Add a `--fail-fast` flag which stops running test cases after the first
failure.

- in the engine, check for failed tests immediately after running them.
  when fast failures are enabled `close()` the engine after any failure.
  Teardowns and cleanups will run, and any concurrently running tests in
  other suits will finish, but no further tests will start.
- add the argument in `configuration` and parse it with the args.
- add a test with a failing case that prevents later tests from running.
  • Loading branch information
natebosch authored Aug 3, 2023
1 parent a9dcce2 commit 5d571d6
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 20 deletions.
4 changes: 4 additions & 0 deletions pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.24.6-wip

* Add support for discontinuing after the first failing test with `--fail-fast`.

## 1.24.5

* Change "compiling <path>" to "loading <path>" message in all cases. Surface
Expand Down
4 changes: 2 additions & 2 deletions pkgs/test/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: test
version: 1.24.5
version: 1.24.6-wip
description: >-
A full featured library for writing and running Dart tests across platforms.
repository: https://github.com/dart-lang/test/tree/master/pkgs/test
Expand Down Expand Up @@ -35,7 +35,7 @@ dependencies:

# Use an exact version until the test_api and test_core package are stable.
test_api: 0.6.1
test_core: 0.5.5
test_core: 0.5.6

typed_data: ^1.3.0
web_socket_channel: ^2.0.0
Expand Down
13 changes: 13 additions & 0 deletions pkgs/test/test/runner/engine_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ void main() {
expect(engine.run(), completion(isFalse));
});

test('.run() does not run more tests after failure for stopOnFirstFailure',
() async {
var secondTestRan = false;
var engine = declareEngine(() {
test('failure', () => throw 'oh no');
test('subsequent', () {
secondTestRan = true;
});
}, stopOnFirstFailure: true);
await expectLater(engine.run(), completion(isFalse));
expect(secondTestRan, false);
});

test('.run() may not be called more than once', () {
var engine = Engine.withSuites([]);
expect(engine.run(), completes);
Expand Down
1 change: 1 addition & 0 deletions pkgs/test/test/runner/runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ $_runtimeCompilers
Must be a 32bit unsigned integer or "random".
If "random", pick a random seed to use.
If not passed, do not randomize test case execution order.
--[no-]fail-fast Stop running tests after the first failure.
Output:
-r, --reporter=<option> Set how to print test results.
Expand Down
27 changes: 18 additions & 9 deletions pkgs/test/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,24 @@ List<GroupEntry> declare(void Function() body) {
}

/// Runs [body] with a declarer and returns an engine that runs those tests.
Engine declareEngine(void Function() body,
{bool runSkipped = false, String? coverage}) {
Engine declareEngine(
void Function() body, {
bool runSkipped = false,
String? coverage,
bool stopOnFirstFailure = false,
}) {
var declarer = Declarer()..declare(body);
return Engine.withSuites([
RunnerSuite(
const PluginEnvironment(),
SuiteConfiguration.runSkipped(runSkipped),
declarer.build(),
suitePlatform)
], coverage: coverage);
return Engine.withSuites(
[
RunnerSuite(
const PluginEnvironment(),
SuiteConfiguration.runSkipped(runSkipped),
declarer.build(),
suitePlatform)
],
coverage: coverage,
stopOnFirstFailure: stopOnFirstFailure,
);
}

/// Returns a [RunnerSuite] with a default environment and configuration.
Expand Down Expand Up @@ -348,6 +356,7 @@ Configuration configuration(
tags: tags,
onPlatform: onPlatform,
testRandomizeOrderingSeed: testRandomizeOrderingSeed,
stopOnFirstFailure: false,
timeout: timeout,
verboseTrace: verboseTrace,
chainStackTraces: chainStackTraces,
Expand Down
4 changes: 4 additions & 0 deletions pkgs/test_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.5.6-wip

* Add support for discontinuing after the first failing test with `--fail-fast`.

## 0.5.5

* Change "compiling <path>" to "loading <path>" message in all cases. Surface
Expand Down
8 changes: 5 additions & 3 deletions pkgs/test_core/lib/src/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ class Runner {
/// Creates a new runner based on [configuration].
factory Runner(Configuration config) => config.asCurrent(() {
var engine = Engine(
concurrency: config.concurrency,
coverage: config.coverage,
testRandomizeOrderingSeed: config.testRandomizeOrderingSeed);
concurrency: config.concurrency,
coverage: config.coverage,
testRandomizeOrderingSeed: config.testRandomizeOrderingSeed,
stopOnFirstFailure: config.stopOnFirstFailure,
);

var sinks = <IOSink>[];
Reporter createFileReporter(String reporterName, String filepath) {
Expand Down
18 changes: 18 additions & 0 deletions pkgs/test_core/lib/src/runner/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ class Configuration {
/// The same seed will shuffle the tests in the same way every time.
final int? testRandomizeOrderingSeed;

final bool? _stopOnFirstFailure;

/// Whether to stop running subsequent tests after a test fails.
bool get stopOnFirstFailure => _stopOnFirstFailure ?? false;

/// Returns the current configuration, or a default configuration if no
/// current configuration is set.
///
Expand Down Expand Up @@ -270,6 +275,7 @@ class Configuration {
required Map<String, CustomRuntime>? defineRuntimes,
required bool? noRetry,
required int? testRandomizeOrderingSeed,
required bool? stopOnFirstFailure,

// Suite-level configuration
required bool? allowDuplicateTestNames,
Expand Down Expand Up @@ -322,6 +328,7 @@ class Configuration {
defineRuntimes: defineRuntimes,
noRetry: noRetry,
testRandomizeOrderingSeed: testRandomizeOrderingSeed,
stopOnFirstFailure: stopOnFirstFailure,
includeTags: includeTags,
excludeTags: excludeTags,
globalPatterns: globalPatterns,
Expand Down Expand Up @@ -379,6 +386,7 @@ class Configuration {
Map<String, CustomRuntime>? defineRuntimes,
bool? noRetry,
int? testRandomizeOrderingSeed,
bool? stopOnFirstFailure,

// Suite-level configuration
bool? allowDuplicateTestNames,
Expand Down Expand Up @@ -430,6 +438,7 @@ class Configuration {
defineRuntimes: defineRuntimes,
noRetry: noRetry,
testRandomizeOrderingSeed: testRandomizeOrderingSeed,
stopOnFirstFailure: stopOnFirstFailure,
allowDuplicateTestNames: allowDuplicateTestNames,
allowTestRandomization: allowTestRandomization,
jsTrace: jsTrace,
Expand Down Expand Up @@ -496,6 +505,7 @@ class Configuration {
defineRuntimes: null,
noRetry: null,
testRandomizeOrderingSeed: null,
stopOnFirstFailure: null,
ignoreTimeouts: null,
allowDuplicateTestNames: null,
allowTestRandomization: null,
Expand Down Expand Up @@ -563,6 +573,7 @@ class Configuration {
defineRuntimes: null,
noRetry: null,
testRandomizeOrderingSeed: null,
stopOnFirstFailure: null,
jsTrace: null,
runSkipped: null,
dart2jsArgs: null,
Expand Down Expand Up @@ -627,6 +638,7 @@ class Configuration {
defineRuntimes: null,
noRetry: null,
testRandomizeOrderingSeed: null,
stopOnFirstFailure: null,
allowDuplicateTestNames: null,
allowTestRandomization: null,
jsTrace: null,
Expand Down Expand Up @@ -689,6 +701,7 @@ class Configuration {
overrideRuntimes: null,
noRetry: null,
testRandomizeOrderingSeed: null,
stopOnFirstFailure: null,
allowDuplicateTestNames: null,
allowTestRandomization: null,
jsTrace: null,
Expand Down Expand Up @@ -754,6 +767,7 @@ class Configuration {
required Map<String, CustomRuntime>? defineRuntimes,
required bool? noRetry,
required this.testRandomizeOrderingSeed,
required bool? stopOnFirstFailure,
required BooleanSelector? includeTags,
required BooleanSelector? excludeTags,
required Iterable<Pattern>? globalPatterns,
Expand Down Expand Up @@ -786,6 +800,7 @@ class Configuration {
globalPatterns = globalPatterns == null
? const {}
: UnmodifiableSetView(globalPatterns.toSet()),
_stopOnFirstFailure = stopOnFirstFailure,
suiteDefaults = (() {
var config = suiteDefaults ?? SuiteConfiguration.empty;
if (pauseAfterLoad == true) {
Expand Down Expand Up @@ -839,6 +854,7 @@ class Configuration {
defineRuntimes: null,
noRetry: null,
testRandomizeOrderingSeed: null,
stopOnFirstFailure: null,
includeTags: null,
excludeTags: null,
);
Expand Down Expand Up @@ -944,6 +960,7 @@ class Configuration {
noRetry: other._noRetry ?? _noRetry,
testRandomizeOrderingSeed:
other.testRandomizeOrderingSeed ?? testRandomizeOrderingSeed,
stopOnFirstFailure: other._stopOnFirstFailure ?? _stopOnFirstFailure,
includeTags: includeTags.intersection(other.includeTags),
excludeTags: excludeTags.union(other.excludeTags),
globalPatterns: globalPatterns.union(other.globalPatterns),
Expand Down Expand Up @@ -1034,6 +1051,7 @@ class Configuration {
noRetry: noRetry ?? _noRetry,
testRandomizeOrderingSeed:
testRandomizeOrderingSeed ?? this.testRandomizeOrderingSeed,
stopOnFirstFailure: _stopOnFirstFailure,
includeTags: includeTags,
excludeTags: excludeTags,
globalPatterns: globalPatterns,
Expand Down
3 changes: 3 additions & 0 deletions pkgs/test_core/lib/src/runner/configuration/args.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ final ArgParser _parser = (() {
'Must be a 32bit unsigned integer or "random".\n'
'If "random", pick a random seed to use.\n'
'If not passed, do not randomize test case execution order.');
parser.addFlag('fail-fast',
help: 'Stop running tests after the first failure.\n');

var reporterDescriptions = <String, String>{};
for (var reporter in allReporters.keys) {
Expand Down Expand Up @@ -348,6 +350,7 @@ class _Parser {
noRetry: _ifParsed('no-retry'),
testRandomizeOrderingSeed: testRandomizeOrderingSeed,
ignoreTimeouts: _ifParsed('ignore-timeouts'),
stopOnFirstFailure: _ifParsed('fail-fast'),
// Config that isn't supported on the command line
addTags: null,
allowTestRandomization: null,
Expand Down
28 changes: 23 additions & 5 deletions pkgs/test_core/lib/src/runner/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class Engine {
/// The same seed will shuffle the tests in the same way every time.
int? testRandomizeOrderingSeed;

/// Whether to stop running tests after a failure.
bool _stopOnFirstFailure;

/// A pool that limits the number of test suites running concurrently.
final Pool _runPool;

Expand Down Expand Up @@ -202,8 +205,16 @@ class Engine {
/// Omitting this argument or passing `0` disables shuffling.
///
/// [coverage] specifies a directory to output coverage information.
Engine({int? concurrency, String? coverage, this.testRandomizeOrderingSeed})
: _runPool = Pool(concurrency ?? 1),
///
/// If [stopOnFirstFailure] then a single failing test will cause the engine
/// to [close] and stop ruunning further tests.
Engine({
int? concurrency,
String? coverage,
this.testRandomizeOrderingSeed,
bool stopOnFirstFailure = false,
}) : _runPool = Pool(concurrency ?? 1),
_stopOnFirstFailure = stopOnFirstFailure,
_coverage = coverage {
_group.future.then((_) {
_onTestStartedGroup.close();
Expand All @@ -222,8 +233,12 @@ class Engine {
/// [concurrency] controls how many suites are run at once. If [runSkipped] is
/// `true`, skipped tests will be run as though they weren't skipped.
factory Engine.withSuites(List<RunnerSuite> suites,
{int? concurrency, String? coverage}) {
var engine = Engine(concurrency: concurrency, coverage: coverage);
{int? concurrency, String? coverage, bool stopOnFirstFailure = false}) {
var engine = Engine(
concurrency: concurrency,
coverage: coverage,
stopOnFirstFailure: stopOnFirstFailure,
);
for (var suite in suites) {
engine.suiteSink.add(suite);
}
Expand Down Expand Up @@ -371,7 +386,10 @@ class Engine {
// loop pump to avoid starving non-microtask events.
await Future(() {});

if (!_restarted.contains(liveTest)) return;
if (!_restarted.contains(liveTest)) {
if (_stopOnFirstFailure && liveTest.state.result.isFailing) close();
return;
}
await _runLiveTest(suiteController, liveTest.copy(),
countSuccess: countSuccess);
_restarted.remove(liveTest);
Expand Down
2 changes: 1 addition & 1 deletion pkgs/test_core/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: test_core
version: 0.5.5
version: 0.5.6-wip
description: A basic library for writing tests and running them on the VM.
repository: https://github.com/dart-lang/test/tree/master/pkgs/test_core

Expand Down

0 comments on commit 5d571d6

Please sign in to comment.