diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 1e3e2b4a03..6260cc9aa8 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -15,6 +15,7 @@ import '../base/async_guard.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; +import '../base/process.dart'; import '../build_info.dart'; import '../cache.dart'; import '../compile.dart'; @@ -307,6 +308,7 @@ class FlutterPlatform extends PlatformPlugin { this.testTimeRecorder, this.nativeAssetsBuilder, this.buildInfo, + this.shutdownHooks, }); final String shellPath; @@ -325,6 +327,7 @@ class FlutterPlatform extends PlatformPlugin { final TestTimeRecorder? testTimeRecorder; final TestCompilerNativeAssetsBuilder? nativeAssetsBuilder; final BuildInfo? buildInfo; + final ShutdownHooks? shutdownHooks; /// 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 - final List finalizers = []; // Will be run in reverse order. + // Will be run in reverse order. + final List finalizers = []; + bool ranFinalizers = false; bool controllerSinkClosed = false; + Future 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 { // Callback can't throw since it's just setting a variable. unawaited(testHarnessChannel.sink.done.whenComplete(() { @@ -608,21 +638,7 @@ class FlutterPlatform extends PlatformPlugin { outOfBandError ??= _AsyncError(reportedError, reportedStackTrace); } } finally { - globals.printTrace('test $ourTestCount: cleaning up...'); - // 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); - } - } - } + await finalize(); if (!controllerSinkClosed) { // Waiting below with await. unawaited(testHarnessChannel.sink.close()); diff --git a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart index 98c9afd81a..6a8d5fbfda 100644 --- a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart @@ -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/io.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/device.dart'; import 'package:flutter_tools/src/flutter_manifest.dart'; @@ -97,6 +98,35 @@ void main() { 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(), + returnsNormally, + ); + + final BufferLogger logger = globals.logger as BufferLogger; + await shutdownHooks.runShutdownHooks(logger); + expect(logger.traceText, contains('test 0: ensuring test device is terminated.')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + ApplicationPackageFactory: () => _FakeApplicationPackageFactory(), + }); + testUsingContext('installHook creates a FlutterPlatform', () { expect(() => installHook( shellPath: 'abc', @@ -214,6 +244,25 @@ class _UnstartableDevice extends Fake implements Device { } } +class _WorkingDevice extends Fake implements Device { + @override + Future dispose() async {} + + @override + Future get targetPlatform => Future.value(TargetPlatform.android); + + @override + Future stopApp(ApplicationPackage? app, {String? userIdentifier}) async => true; + + @override + Future uninstallApp(ApplicationPackage app, {String? userIdentifier}) async => true; + + @override + Future startApp(covariant ApplicationPackage? package, {String? mainPath, String? route, required DebuggingOptions debuggingOptions, Map platformArgs = const {}, 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 { @override FlutterManifest get manifest => FlutterManifest.empty(logger: BufferLogger.test());