Disallow exiting from VM tests (#2640)
As suggested in #2577
Tests exiting early with a hard exit is an infrequent but persistent
source of confusion. Use `IOOverrides` to treat any call to `exit` as a
test failure instead of a VM exit.
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index 66d75f8..0cfc58a 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -10,6 +10,7 @@
independent from any OS specific configuration. For instance a wide skip of
all tests with OS `'windows'` would previously still run browser tests on
windows, but will now skip all tests including browser tests.
+* Treat calls to `exit` as test failures in VM tests.
## 1.31.1
diff --git a/pkgs/test/test/runner/subprocess_crash_test.dart b/pkgs/test/test/runner/subprocess_crash_test.dart
index e98f634..b6e40df 100644
--- a/pkgs/test/test/runner/subprocess_crash_test.dart
+++ b/pkgs/test/test/runner/subprocess_crash_test.dart
@@ -13,16 +13,16 @@
void main() {
setUpAll(precompileTestExecutable);
- test('gracefully handles an early test suite exit', () async {
+ test('gracefully handles a non-exit crash', () async {
await d.file('test.dart', '''
- import 'dart:io';
+ import 'dart:ffi';
import 'package:test/test.dart';
void main() {
test('runs', () {});
- test('exits', () {
- exit(0);
+ test('crashes', () {
+ Pointer.fromAddress(0).cast<Long>()[0] = 0;
});
}''').create();
@@ -30,7 +30,7 @@
expect(
test.stdout,
containsInOrder([
- '+1: [VM, Exe] exits - did not complete [E]',
+ '+1: [VM, Exe] crashes - did not complete [E]',
'+1: Some tests failed.',
]),
);
diff --git a/pkgs/test/test/runner/vm/runner_test.dart b/pkgs/test/test/runner/vm/runner_test.dart
new file mode 100644
index 0000000..7f795b6
--- /dev/null
+++ b/pkgs/test/test/runner/vm/runner_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2026, 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+ setUpAll(precompileTestExecutable);
+
+ group('fails gracefully if a test file calls exit(0)', () {
+ setUp(() async {
+ await d.file('test.dart', '''
+import 'dart:io';
+import 'package:test/test.dart';
+
+void main() {
+ test('exits', () {
+ exit(0);
+ });
+}
+''').create();
+ });
+
+ test('in a VM test', () async {
+ var test = await runTest(['test.dart']);
+
+ expect(test.stdout, containsInOrder(['exit(0) was called.']));
+ await test.shouldExit(1);
+ });
+
+ test('in a native test', () async {
+ var test = await runTest(['--compiler', 'exe', 'test.dart']);
+
+ expect(test.stdout, containsInOrder(['exit(0) was called.']));
+ await test.shouldExit(1);
+ });
+ });
+}
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index b0e5659..608cc4b 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,6 +1,7 @@
## 0.6.19-wip
* Support using the OS platform selector to configure browser tests.
+* Treat calls to `exit` as test failures in VM tests.
## 0.6.18
diff --git a/pkgs/test_core/lib/src/bootstrap/vm.dart b/pkgs/test_core/lib/src/bootstrap/vm.dart
index a4eebe6..034a0ab 100644
--- a/pkgs/test_core/lib/src/bootstrap/vm.dart
+++ b/pkgs/test_core/lib/src/bootstrap/vm.dart
@@ -9,6 +9,7 @@
import 'package:stream_channel/isolate_channel.dart';
import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/hooks.dart';
import '../runner/plugin/remote_platform_helpers.dart';
import '../runner/plugin/shared_platform_helpers.dart';
@@ -19,7 +20,12 @@
IsolateChannel<Object?>.connectSend(sendPort),
);
var testControlChannel = platformChannel.virtualChannel()
- ..pipe(serializeSuite(getMain));
+ ..pipe(
+ IOOverrides.runWithIOOverrides(
+ () => serializeSuite(getMain),
+ _EarlyExitIOOverrides(),
+ ),
+ );
platformChannel.sink.add(testControlChannel.id);
platformChannel.stream.forEach((message) {
@@ -43,7 +49,12 @@
var socket = await Socket.connect(args[0], int.parse(args[1]));
var platformChannel = MultiChannel<Object?>(jsonSocketStreamChannel(socket));
var testControlChannel = platformChannel.virtualChannel()
- ..pipe(serializeSuite(getMain));
+ ..pipe(
+ IOOverrides.runWithIOOverrides(
+ () => serializeSuite(getMain),
+ _EarlyExitIOOverrides(),
+ ),
+ );
platformChannel.sink.add(testControlChannel.id);
unawaited(
@@ -54,3 +65,10 @@
}),
);
}
+
+final class _EarlyExitIOOverrides extends IOOverrides {
+ @override
+ Never exit(int code) {
+ throw TestFailure('exit($code) was called.');
+ }
+}