Provide better error messages in integration tests when apps don't launch successfully (#20813)

* Don't keep parsing results once we've had the event we expected

* Report errors if an app.stop event is received while waiting for another event

* Don't throw if we see app.stop event when we've just sent an app.stop request

* Improve debug print to include --start-paused if being used

* Improve wrapping
diff --git a/packages/flutter_tools/test/integration/test_driver.dart b/packages/flutter_tools/test/integration/test_driver.dart
index ee9a76d..078d3fa 100644
--- a/packages/flutter_tools/test/integration/test_driver.dart
+++ b/packages/flutter_tools/test/integration/test_driver.dart
@@ -76,13 +76,15 @@
 
   Future<void> _setupProcess(List<String> args, {bool withDebugger = false, bool pauseOnExceptions = false}) async {
     final String flutterBin = fs.path.join(getFlutterRoot(), 'bin', 'flutter');
-    _debugPrint('Spawning flutter $args in ${_projectFolder.path}');
+    final List<String> flutterArgs = withDebugger
+        ? args.followedBy(<String>['--start-paused']).toList()
+        : args;
+    _debugPrint('Spawning flutter $flutterArgs in ${_projectFolder.path}');
 
     const ProcessManager _processManager = LocalProcessManager();
     _proc = await _processManager.start(
         <String>[flutterBin]
-            .followedBy(args)
-            .followedBy(withDebugger ? <String>['--start-paused'] : <String>[])
+            .followedBy(flutterArgs)
             .toList(),
         workingDirectory: _projectFolder.path,
         environment: <String, String>{'FLUTTER_TEST': 'true'});
@@ -263,16 +265,35 @@
     return script.sourceLocation(frame.location.token);
   }
 
-  Future<Map<String, dynamic>> _waitFor({String event, int id, Duration timeout}) async {
+  Future<Map<String, dynamic>> _waitFor({
+    String event,
+    int id,
+    Duration timeout,
+    bool ignoreAppStopEvent = false,
+  }) async {
     final Completer<Map<String, dynamic>> response = new Completer<Map<String, dynamic>>();
-    final StreamSubscription<String> sub = _stdout.stream.listen((String line) {
+    StreamSubscription<String> sub;
+    sub = _stdout.stream.listen((String line) async {
       final dynamic json = _parseFlutterResponse(line);
       if (json == null) {
         return;
       } else if (
           (event != null && json['event'] == event)
           || (id != null && json['id'] == id)) {
+        await sub.cancel();
         response.complete(json);
+      } else if (!ignoreAppStopEvent && json['event'] == 'app.stop') {
+        await sub.cancel();
+        final StringBuffer error = new StringBuffer();
+        error.write('Received app.stop event while waiting for ');
+        error.write('${event != null ? '$event event' : 'response to request $id.'}.\n\n');
+        if (json['params'] != null && json['params']['error'] != null) {
+          error.write('${json['params']['error']}\n\n');
+        }
+        if (json['params'] != null && json['params']['trace'] != null) {
+          error.write('${json['params']['trace']}\n\n');
+        }
+        response.completeError(error.toString());
       }
     });
 
@@ -327,8 +348,12 @@
     _debugPrint(jsonEncoded);
 
     // Set up the response future before we send the request to avoid any
-    // races.
-    final Future<Map<String, dynamic>> responseFuture = _waitFor(id: requestId);
+    // races. If the method we're calling is app.stop then we tell waitFor not
+    // to throw if it sees an app.stop event before the response to this request.
+    final Future<Map<String, dynamic>> responseFuture = _waitFor(
+      id: requestId,
+      ignoreAppStopEvent: method == 'app.stop',
+    );
     _proc.stdin.writeln(jsonEncoded);
     final Map<String, dynamic> response = await responseFuture;