diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 82967b30ca..18bf8fe98f 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -11,6 +11,7 @@ import 'package:package_config/package_config.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:test_core/src/platform.dart'; // ignore: implementation_imports +import '../base/async_guard.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -459,6 +460,15 @@ class FlutterPlatform extends PlatformPlugin { ); } + void _handleStartedDevice(Uri? uri, int testCount) { + if (uri != null) { + globals.printTrace('test $testCount: VM Service uri is available at $uri'); + } else { + globals.printTrace('test $testCount: VM Service uri is not available'); + } + watcher?.handleStartedDevice(uri); + } + Future<_AsyncError?> _startTest( String testPath, StreamChannel testHarnessChannel, @@ -530,7 +540,16 @@ class FlutterPlatform extends PlatformPlugin { globals.printTrace('test $ourTestCount: starting test device'); final TestDevice testDevice = _createTestDevice(ourTestCount); final Stopwatch? testTimeRecorderStopwatch = testTimeRecorder?.start(TestTimePhases.Run); - final Future> remoteChannelFuture = testDevice.start(mainDart!); + final Completer> remoteChannelCompleter = Completer>(); + unawaited(asyncGuard( + () async { + final StreamChannel channel = await testDevice.start(mainDart!); + remoteChannelCompleter.complete(channel); + }, + onError: (Object err, StackTrace stackTrace) { + remoteChannelCompleter.completeError(err, stackTrace); + }, + )); finalizers.add(() async { globals.printTrace('test $ourTestCount: ensuring test device is terminated.'); await testDevice.kill(); @@ -545,15 +564,21 @@ class FlutterPlatform extends PlatformPlugin { await Future.any(>[ testDevice.finished, () async { - final Uri? processVmServiceUri = await testDevice.vmServiceUri; - if (processVmServiceUri != null) { - globals.printTrace('test $ourTestCount: VM Service uri is available at $processVmServiceUri'); - } else { - globals.printTrace('test $ourTestCount: VM Service uri is not available'); - } - watcher?.handleStartedDevice(processVmServiceUri); + final [Object? first, Object? _] = await Future.wait( + >[ + // This future may depend on [_handleStartedDevice] having been called + remoteChannelCompleter.future, + testDevice.vmServiceUri.then((Uri? processVmServiceUri) { + _handleStartedDevice(processVmServiceUri, ourTestCount); + }), + ], + // If [remoteChannelCompleter.future] errors, we may never get the + // VM service URI, so erroring eagerly is necessary to avoid a + // deadlock. + eagerError: true, + ); + final StreamChannel remoteChannel = first! as StreamChannel; - final StreamChannel remoteChannel = await remoteChannelFuture; globals.printTrace('test $ourTestCount: connected to test device, now awaiting test result'); await _pipeHarnessToRemote( 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 ed4fa844a2..98c9afd81a 100644 --- a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart @@ -3,11 +3,17 @@ // found in the LICENSE file. import 'package:file/memory.dart'; +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/build_info.dart'; import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/flutter_manifest.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/test/flutter_platform.dart'; +import 'package:test/fake.dart'; import 'package:test_core/backend.dart'; import '../src/common.dart'; @@ -63,6 +69,34 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); + testUsingContext('an exception from the app not starting bubbles up to the test runner', () async { + final _UnstartableDevice testDevice = _UnstartableDevice(); + final FlutterPlatform flutterPlatform = FlutterPlatform( + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + shellPath: '/', + enableVmService: false, + integrationTestDevice: testDevice, + flutterProject: _FakeFlutterProject(), + host: InternetAddress.anyIPv4, + updateGoldens: false, + ); + + await expectLater( + () => flutterPlatform.loadChannel('test1.dart', fakeSuitePlatform).stream.drain(), + // we intercept the actual exception and throw a string for the test runner to catch + throwsA(isA().having( + (String msg) => msg, + 'string', + 'Unable to start the app on the device.', + )), + ); + expect((globals.logger as BufferLogger).traceText, contains('test 0: error caught during test;')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + ApplicationPackageFactory: () => _FakeApplicationPackageFactory(), + }); + testUsingContext('installHook creates a FlutterPlatform', () { expect(() => installHook( shellPath: 'abc', @@ -158,3 +192,44 @@ void main() { }); }); } + +class _UnstartableDevice extends Fake implements Device { + @override + Future dispose() => Future.value(); + + @override + Future get targetPlatform => Future.value(TargetPlatform.android); + + @override + Future stopApp(ApplicationPackage? app, {String? userIdentifier}) async { + return 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.failed(); + } +} + +class _FakeFlutterProject extends Fake implements FlutterProject { + @override + FlutterManifest get manifest => FlutterManifest.empty(logger: BufferLogger.test()); +} + +class _FakeApplicationPackageFactory implements ApplicationPackageFactory { + TargetPlatform? platformRequested; + File? applicationBinaryRequested; + ApplicationPackage applicationPackage = _FakeApplicationPackage(); + + @override + Future getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async { + platformRequested = platform; + applicationBinaryRequested = applicationBinary; + return applicationPackage; + } +} + +class _FakeApplicationPackage extends Fake implements ApplicationPackage {}