Terminate the test device if the flutter
tool is signal-killed. (#159115)
Closes https://github.com/flutter/flutter/issues/20949. Signals (such as SIGTERM or SIGKILL) end up flowing through `exitWithHooks`, which in turn, after running hooks, call `exit().` That means, as a result, any `try { } finally { }` guarded execution may _not_ run, which happens to also be how `flutter_tester` instances are cleaned up if they have not terminated. This PR adds in-progress `flutter_tester` runs (or any platform `flutter_platform` supports) to the shutdown hooks, guaranteeing that the finalizers (which in turn, kill the process) are _always_ executed as long as either the test completes, _or_ `exitWithHooks` is called. The existing integration tests (`integration.shard/test_test.dart`) still pass as well.
This commit is contained in:
parent
0e1c6330ea
commit
c5379557b3
@ -15,6 +15,7 @@ import '../base/async_guard.dart';
|
|||||||
import '../base/common.dart';
|
import '../base/common.dart';
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
import '../base/io.dart';
|
import '../base/io.dart';
|
||||||
|
import '../base/process.dart';
|
||||||
import '../build_info.dart';
|
import '../build_info.dart';
|
||||||
import '../cache.dart';
|
import '../cache.dart';
|
||||||
import '../compile.dart';
|
import '../compile.dart';
|
||||||
@ -307,6 +308,7 @@ class FlutterPlatform extends PlatformPlugin {
|
|||||||
this.testTimeRecorder,
|
this.testTimeRecorder,
|
||||||
this.nativeAssetsBuilder,
|
this.nativeAssetsBuilder,
|
||||||
this.buildInfo,
|
this.buildInfo,
|
||||||
|
this.shutdownHooks,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String shellPath;
|
final String shellPath;
|
||||||
@ -325,6 +327,7 @@ class FlutterPlatform extends PlatformPlugin {
|
|||||||
final TestTimeRecorder? testTimeRecorder;
|
final TestTimeRecorder? testTimeRecorder;
|
||||||
final TestCompilerNativeAssetsBuilder? nativeAssetsBuilder;
|
final TestCompilerNativeAssetsBuilder? nativeAssetsBuilder;
|
||||||
final BuildInfo? buildInfo;
|
final BuildInfo? buildInfo;
|
||||||
|
final ShutdownHooks? shutdownHooks;
|
||||||
|
|
||||||
/// The device to run the test on for Integration Tests.
|
/// The device to run the test on for Integration Tests.
|
||||||
///
|
///
|
||||||
@ -476,8 +479,35 @@ class FlutterPlatform extends PlatformPlugin {
|
|||||||
|
|
||||||
_AsyncError? outOfBandError; // error that we couldn't send to the harness that we need to send via our future
|
_AsyncError? outOfBandError; // error that we couldn't send to the harness that we need to send via our future
|
||||||
|
|
||||||
final List<Finalizer> finalizers = <Finalizer>[]; // Will be run in reverse order.
|
// Will be run in reverse order.
|
||||||
|
final List<Finalizer> finalizers = <Finalizer>[];
|
||||||
|
bool ranFinalizers = false;
|
||||||
bool controllerSinkClosed = false;
|
bool controllerSinkClosed = false;
|
||||||
|
Future<void> finalize() async {
|
||||||
|
if (ranFinalizers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ranFinalizers = true;
|
||||||
|
globals.printTrace('test $ourTestCount: cleaning up...');
|
||||||
|
for (final Finalizer finalizer in finalizers.reversed) {
|
||||||
|
try {
|
||||||
|
await finalizer();
|
||||||
|
} on Exception catch (error, stack) {
|
||||||
|
globals.printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
|
||||||
|
if (!controllerSinkClosed) {
|
||||||
|
testHarnessChannel.sink.addError(error, stack);
|
||||||
|
} else {
|
||||||
|
globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
|
||||||
|
outOfBandError ??= _AsyncError(error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the flutter CLI is forcibly terminated, cleanup processes.
|
||||||
|
final ShutdownHooks shutdownHooks = this.shutdownHooks ?? globals.shutdownHooks;
|
||||||
|
shutdownHooks.addShutdownHook(finalize);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Callback can't throw since it's just setting a variable.
|
// Callback can't throw since it's just setting a variable.
|
||||||
unawaited(testHarnessChannel.sink.done.whenComplete(() {
|
unawaited(testHarnessChannel.sink.done.whenComplete(() {
|
||||||
@ -608,21 +638,7 @@ class FlutterPlatform extends PlatformPlugin {
|
|||||||
outOfBandError ??= _AsyncError(reportedError, reportedStackTrace);
|
outOfBandError ??= _AsyncError(reportedError, reportedStackTrace);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
globals.printTrace('test $ourTestCount: cleaning up...');
|
await finalize();
|
||||||
// Finalizers are treated like a stack; run them in reverse order.
|
|
||||||
for (final Finalizer finalizer in finalizers.reversed) {
|
|
||||||
try {
|
|
||||||
await finalizer();
|
|
||||||
} on Exception catch (error, stack) {
|
|
||||||
globals.printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
|
|
||||||
if (!controllerSinkClosed) {
|
|
||||||
testHarnessChannel.sink.addError(error, stack);
|
|
||||||
} else {
|
|
||||||
globals.printError('unhandled error during finalization of test:\n$testPath\n$error\n$stack');
|
|
||||||
outOfBandError ??= _AsyncError(error, stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!controllerSinkClosed) {
|
if (!controllerSinkClosed) {
|
||||||
// Waiting below with await.
|
// Waiting below with await.
|
||||||
unawaited(testHarnessChannel.sink.close());
|
unawaited(testHarnessChannel.sink.close());
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter_tools/src/application_package.dart';
|
|||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
import 'package:flutter_tools/src/base/logger.dart';
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
|
import 'package:flutter_tools/src/base/process.dart';
|
||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
import 'package:flutter_tools/src/flutter_manifest.dart';
|
import 'package:flutter_tools/src/flutter_manifest.dart';
|
||||||
@ -97,6 +98,35 @@ void main() {
|
|||||||
ApplicationPackageFactory: () => _FakeApplicationPackageFactory(),
|
ApplicationPackageFactory: () => _FakeApplicationPackageFactory(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUsingContext('a shutdown signal terminates the test device', () async {
|
||||||
|
final _WorkingDevice testDevice = _WorkingDevice();
|
||||||
|
|
||||||
|
final ShutdownHooks shutdownHooks = ShutdownHooks();
|
||||||
|
final FlutterPlatform flutterPlatform = FlutterPlatform(
|
||||||
|
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
|
||||||
|
shellPath: '/',
|
||||||
|
enableVmService: false,
|
||||||
|
integrationTestDevice: testDevice,
|
||||||
|
flutterProject: _FakeFlutterProject(),
|
||||||
|
host: InternetAddress.anyIPv4,
|
||||||
|
updateGoldens: false,
|
||||||
|
shutdownHooks: shutdownHooks,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
() => flutterPlatform.loadChannel('test1.dart', fakeSuitePlatform).stream.drain<void>(),
|
||||||
|
returnsNormally,
|
||||||
|
);
|
||||||
|
|
||||||
|
final BufferLogger logger = globals.logger as BufferLogger;
|
||||||
|
await shutdownHooks.runShutdownHooks(logger);
|
||||||
|
expect(logger.traceText, contains('test 0: ensuring test device is terminated.'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
ApplicationPackageFactory: () => _FakeApplicationPackageFactory(),
|
||||||
|
});
|
||||||
|
|
||||||
testUsingContext('installHook creates a FlutterPlatform', () {
|
testUsingContext('installHook creates a FlutterPlatform', () {
|
||||||
expect(() => installHook(
|
expect(() => installHook(
|
||||||
shellPath: 'abc',
|
shellPath: 'abc',
|
||||||
@ -214,6 +244,25 @@ class _UnstartableDevice extends Fake implements Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _WorkingDevice extends Fake implements Device {
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TargetPlatform> get targetPlatform => Future<TargetPlatform>.value(TargetPlatform.android);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> stopApp(ApplicationPackage? app, {String? userIdentifier}) async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> uninstallApp(ApplicationPackage app, {String? userIdentifier}) async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LaunchResult> startApp(covariant ApplicationPackage? package, {String? mainPath, String? route, required DebuggingOptions debuggingOptions, Map<String, Object?> platformArgs = const <String, Object>{}, bool prebuiltApplication = false, String? userIdentifier}) async {
|
||||||
|
return LaunchResult.succeeded(vmServiceUri: Uri.parse('http://127.0.0.1:12345/vmService'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FakeFlutterProject extends Fake implements FlutterProject {
|
class _FakeFlutterProject extends Fake implements FlutterProject {
|
||||||
@override
|
@override
|
||||||
FlutterManifest get manifest => FlutterManifest.empty(logger: BufferLogger.test());
|
FlutterManifest get manifest => FlutterManifest.empty(logger: BufferLogger.test());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user