Refactor Test Driver in preparation for `flutter test` integration tests (#24060)

* Refactor Test Driver in preperation for `flutter test` integration tests

* Fix indent
diff --git a/packages/flutter_tools/test/integration/expression_evaluation_test.dart b/packages/flutter_tools/test/integration/expression_evaluation_test.dart
index eb6bb3c..6610ee6 100644
--- a/packages/flutter_tools/test/integration/expression_evaluation_test.dart
+++ b/packages/flutter_tools/test/integration/expression_evaluation_test.dart
@@ -15,15 +15,15 @@
 import 'test_utils.dart';
 
 void main() {
-  group('expression evaluation', () {
+  group('flutter run expression evaluation', () {
     Directory tempDir;
     final BasicProject _project = BasicProject();
-    FlutterTestDriver _flutter;
+    FlutterRunTestDriver _flutter;
 
     setUp(() async {
       tempDir = createResolvedTempDirectorySync();
       await _project.setUpIn(tempDir);
-      _flutter = FlutterTestDriver(tempDir);
+      _flutter = FlutterRunTestDriver(tempDir);
     });
 
     tearDown(() async {
@@ -43,70 +43,70 @@
           _project.topLevelFunctionBreakpointLine);
     }
 
-    Future<void> evaluateTrivialExpressions() async {
-      InstanceRef res;
-
-      res = await _flutter.evaluateInFrame('"test"');
-      expect(res.kind == InstanceKind.kString && res.valueAsString == 'test', isTrue);
-
-      res = await _flutter.evaluateInFrame('1');
-      expect(res.kind == InstanceKind.kInt && res.valueAsString == 1.toString(), isTrue);
-
-      res = await _flutter.evaluateInFrame('true');
-      expect(res.kind == InstanceKind.kBool && res.valueAsString == true.toString(), isTrue);
-    }
-
-    Future<void> evaluateComplexExpressions() async {
-      final InstanceRef res = await _flutter.evaluateInFrame('new DateTime.now().year');
-      expect(res.kind == InstanceKind.kInt && res.valueAsString == DateTime.now().year.toString(), isTrue);
-    }
-
-    Future<void> evaluateComplexReturningExpressions() async {
-      final DateTime now = DateTime.now();
-      final InstanceRef resp = await _flutter.evaluateInFrame('new DateTime.now()');
-      expect(resp.classRef.name, equals('DateTime'));
-      // Ensure we got a reasonable approximation. The more accurate we try to
-      // make this, the more likely it'll fail due to differences in the time
-      // in the remote VM and the local VM at the time the code runs.
-      final InstanceRef res = await _flutter.evaluate(resp.id, r'"$year-$month-$day"');
-      expect(res.valueAsString,
-          equals('${now.year}-${now.month}-${now.day}'));
-    }
-
     test('can evaluate trivial expressions in top level function', () async {
       await _flutter.run(withDebugger: true);
       await breakInTopLevelFunction(_flutter);
-      await evaluateTrivialExpressions();
+      await evaluateTrivialExpressions(_flutter);
     });
 
     test('can evaluate trivial expressions in build method', () async {
       await _flutter.run(withDebugger: true);
       await breakInBuildMethod(_flutter);
-      await evaluateTrivialExpressions();
+      await evaluateTrivialExpressions(_flutter);
     });
 
     test('can evaluate complex expressions in top level function', () async {
       await _flutter.run(withDebugger: true);
       await breakInTopLevelFunction(_flutter);
-      await evaluateComplexExpressions();
+      await evaluateComplexExpressions(_flutter);
     });
 
     test('can evaluate complex expressions in build method', () async {
       await _flutter.run(withDebugger: true);
       await breakInBuildMethod(_flutter);
-      await evaluateComplexExpressions();
+      await evaluateComplexExpressions(_flutter);
     });
 
     test('can evaluate expressions returning complex objects in top level function', () async {
       await _flutter.run(withDebugger: true);
       await breakInTopLevelFunction(_flutter);
-      await evaluateComplexReturningExpressions();
+      await evaluateComplexReturningExpressions(_flutter);
     });
 
     test('can evaluate expressions returning complex objects in build method', () async {
       await _flutter.run(withDebugger: true);
       await breakInBuildMethod(_flutter);
-      await evaluateComplexReturningExpressions();
+      await evaluateComplexReturningExpressions(_flutter);
     });
   }, timeout: const Timeout.factor(6));
 }
+
+Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
+  InstanceRef res;
+
+  res = await flutter.evaluateInFrame('"test"');
+  expect(res.kind == InstanceKind.kString && res.valueAsString == 'test', isTrue);
+
+  res = await flutter.evaluateInFrame('1');
+  expect(res.kind == InstanceKind.kInt && res.valueAsString == 1.toString(), isTrue);
+
+  res = await flutter.evaluateInFrame('true');
+  expect(res.kind == InstanceKind.kBool && res.valueAsString == true.toString(), isTrue);
+}
+
+Future<void> evaluateComplexExpressions(FlutterTestDriver flutter) async {
+  final InstanceRef res = await flutter.evaluateInFrame('new DateTime.now().year');
+    expect(res.kind == InstanceKind.kInt && res.valueAsString == DateTime.now().year.toString(), isTrue);
+}
+
+Future<void> evaluateComplexReturningExpressions(FlutterTestDriver flutter) async {
+  final DateTime now = DateTime.now();
+    final InstanceRef resp = await flutter.evaluateInFrame('new DateTime.now()');
+    expect(resp.classRef.name, equals('DateTime'));
+    // Ensure we got a reasonable approximation. The more accurate we try to
+    // make this, the more likely it'll fail due to differences in the time
+    // in the remote VM and the local VM at the time the code runs.
+    final InstanceRef res = await flutter.evaluate(resp.id, r'"$year-$month-$day"');
+    expect(res.valueAsString,
+        equals('${now.year}-${now.month}-${now.day}'));
+}
diff --git a/packages/flutter_tools/test/integration/flutter_attach_test.dart b/packages/flutter_tools/test/integration/flutter_attach_test.dart
index 2e21ffa..dff1132 100644
--- a/packages/flutter_tools/test/integration/flutter_attach_test.dart
+++ b/packages/flutter_tools/test/integration/flutter_attach_test.dart
@@ -11,15 +11,15 @@
 import 'test_utils.dart';
 
 void main() {
-  FlutterTestDriver _flutterRun, _flutterAttach;
+  FlutterRunTestDriver _flutterRun, _flutterAttach;
   final BasicProject _project = BasicProject();
   Directory tempDir;
 
   setUp(() async {
     tempDir = createResolvedTempDirectorySync();
     await _project.setUpIn(tempDir);
-    _flutterRun = FlutterTestDriver(tempDir, logPrefix: 'RUN');
-    _flutterAttach = FlutterTestDriver(tempDir, logPrefix: 'ATTACH');
+    _flutterRun = FlutterRunTestDriver(tempDir, logPrefix: 'RUN');
+    _flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH');
   });
 
   tearDown(() async {
@@ -54,7 +54,7 @@
       await _flutterRun.run(withDebugger: true);
       await _flutterAttach.attach(_flutterRun.vmServicePort);
       await _flutterAttach.quit();
-      _flutterAttach = FlutterTestDriver(tempDir, logPrefix: 'ATTACH-2');
+      _flutterAttach = FlutterRunTestDriver(tempDir, logPrefix: 'ATTACH-2');
       await _flutterAttach.attach(_flutterRun.vmServicePort);
       await _flutterAttach.hotReload();
     });
diff --git a/packages/flutter_tools/test/integration/flutter_run_test.dart b/packages/flutter_tools/test/integration/flutter_run_test.dart
index 1b31405..70c69d1 100644
--- a/packages/flutter_tools/test/integration/flutter_run_test.dart
+++ b/packages/flutter_tools/test/integration/flutter_run_test.dart
@@ -16,12 +16,12 @@
   group('flutter_run', () {
     Directory tempDir;
     final BasicProject _project = BasicProject();
-    FlutterTestDriver _flutter;
+    FlutterRunTestDriver _flutter;
 
     setUp(() async {
       tempDir = createResolvedTempDirectorySync();
       await _project.setUpIn(tempDir);
-      _flutter = FlutterTestDriver(tempDir);
+      _flutter = FlutterRunTestDriver(tempDir);
     });
 
     tearDown(() async {
diff --git a/packages/flutter_tools/test/integration/hot_reload_test.dart b/packages/flutter_tools/test/integration/hot_reload_test.dart
index 46c3d30..b7e9602 100644
--- a/packages/flutter_tools/test/integration/hot_reload_test.dart
+++ b/packages/flutter_tools/test/integration/hot_reload_test.dart
@@ -17,12 +17,12 @@
   group('hot', () {
     Directory tempDir;
     final HotReloadProject _project = HotReloadProject();
-    FlutterTestDriver _flutter;
+    FlutterRunTestDriver _flutter;
 
     setUp(() async {
       tempDir = createResolvedTempDirectorySync();
       await _project.setUpIn(tempDir);
-      _flutter = FlutterTestDriver(tempDir);
+      _flutter = FlutterRunTestDriver(tempDir);
     });
 
     tearDown(() async {
diff --git a/packages/flutter_tools/test/integration/lifetime_test.dart b/packages/flutter_tools/test/integration/lifetime_test.dart
index aab6d1a..f8faa03 100644
--- a/packages/flutter_tools/test/integration/lifetime_test.dart
+++ b/packages/flutter_tools/test/integration/lifetime_test.dart
@@ -20,13 +20,13 @@
 void main() {
   group('flutter run', () {
     final BasicProject _project = BasicProject();
-    FlutterTestDriver _flutter;
+    FlutterRunTestDriver _flutter;
     Directory tempDir;
 
     setUp(() async {
       tempDir = createResolvedTempDirectorySync();
       await _project.setUpIn(tempDir);
-      _flutter = FlutterTestDriver(tempDir);
+      _flutter = FlutterRunTestDriver(tempDir);
     });
 
     tearDown(() async {
diff --git a/packages/flutter_tools/test/integration/test_data/basic_project.dart b/packages/flutter_tools/test/integration/test_data/basic_project.dart
index 4aa8074..b315f05 100644
--- a/packages/flutter_tools/test/integration/test_data/basic_project.dart
+++ b/packages/flutter_tools/test/integration/test_data/basic_project.dart
@@ -2,9 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'test_project.dart';
+import 'project.dart';
 
-class BasicProject extends TestProject {
+class BasicProject extends Project {
 
   @override
   final String pubspec = '''
diff --git a/packages/flutter_tools/test/integration/test_data/hot_reload_project.dart b/packages/flutter_tools/test/integration/test_data/hot_reload_project.dart
index 0e99ba3..f4174ed 100644
--- a/packages/flutter_tools/test/integration/test_data/hot_reload_project.dart
+++ b/packages/flutter_tools/test/integration/test_data/hot_reload_project.dart
@@ -5,9 +5,9 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 
 import '../test_utils.dart';
-import 'test_project.dart';
+import 'project.dart';
 
-class HotReloadProject extends TestProject {
+class HotReloadProject extends Project {
   @override
   final String pubspec = '''
   name: test
diff --git a/packages/flutter_tools/test/integration/test_data/test_project.dart b/packages/flutter_tools/test/integration/test_data/project.dart
similarity index 89%
rename from packages/flutter_tools/test/integration/test_data/test_project.dart
rename to packages/flutter_tools/test/integration/test_data/project.dart
index 5428177..e723aff 100644
--- a/packages/flutter_tools/test/integration/test_data/test_project.dart
+++ b/packages/flutter_tools/test/integration/test_data/project.dart
@@ -9,7 +9,7 @@
 
 import '../test_utils.dart';
 
-abstract class TestProject {
+abstract class Project {
   Directory dir;
 
   String get pubspec;
@@ -22,7 +22,9 @@
   Future<void> setUpIn(Directory dir) async {
     this.dir = dir;
     writeFile(fs.path.join(dir.path, 'pubspec.yaml'), pubspec);
-    writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), main);
+    if (main != null) {
+      writeFile(fs.path.join(dir.path, 'lib', 'main.dart'), main);
+    }
     await getPackages(dir.path);
   }
 
diff --git a/packages/flutter_tools/test/integration/test_driver.dart b/packages/flutter_tools/test/integration/test_driver.dart
index 0a596bf..0778064 100644
--- a/packages/flutter_tools/test/integration/test_driver.dart
+++ b/packages/flutter_tools/test/integration/test_driver.dart
@@ -20,7 +20,7 @@
 const Duration appStartTimeout = Duration(seconds: 120);
 const Duration quitTimeout = Duration(seconds: 10);
 
-class FlutterTestDriver {
+abstract class FlutterTestDriver {
   FlutterTestDriver(this._projectFolder, {String logPrefix}):
     _logPrefix = logPrefix != null ? '$logPrefix: ' : '';
 
@@ -33,7 +33,6 @@
   final StreamController<String> _allMessages = StreamController<String>.broadcast();
   final StringBuffer _errorBuffer = StringBuffer();
   String _lastResponse;
-  String _currentRunningAppId;
   Uri _vmServiceWsUri;
   bool _hasExited = false;
 
@@ -54,35 +53,6 @@
     return msg;
   }
 
-  Future<void> run({
-    bool withDebugger = false,
-    bool pauseOnExceptions = false,
-    File pidFile,
-  }) async {
-    await _setupProcess(<String>[
-        'run',
-        '--machine',
-        '-d',
-        'flutter-tester',
-    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
-  }
-
-  Future<void> attach(
-    int port, {
-    bool withDebugger = false,
-    bool pauseOnExceptions = false,
-    File pidFile,
-  }) async {
-    await _setupProcess(<String>[
-        'attach',
-        '--machine',
-        '-d',
-        'flutter-tester',
-        '--debug-port',
-        '$port',
-    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
-  }
-
   Future<void> _setupProcess(
     List<String> args, {
     bool withDebugger = false,
@@ -121,110 +91,6 @@
     // This is just debug printing to aid running/debugging tests locally.
     _stdout.stream.listen(_debugPrint);
     _stderr.stream.listen(_debugPrint);
-
-    // Stash the PID so that we can terminate the VM more reliably than using
-    // _proc.kill() (because _proc is a shell, because `flutter` is a shell
-    // script).
-    final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
-    _procPid = connected['params']['pid'];
-
-    // Set this up now, but we don't wait it yet. We want to make sure we don't
-    // miss it while waiting for debugPort below.
-    final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started',
-        timeout: appStartTimeout);
-
-    if (withDebugger) {
-      final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort',
-          timeout: appStartTimeout);
-      final String wsUriString = debugPort['params']['wsUri'];
-      _vmServiceWsUri = Uri.parse(wsUriString);
-      _vmService =
-          await vmServiceConnectUri(_vmServiceWsUri.toString());
-      _vmService.onSend.listen((String s) => _debugPrint('==> $s'));
-      _vmService.onReceive.listen((String s) => _debugPrint('<== $s'));
-      await Future.wait(<Future<Success>>[
-        _vmService.streamListen('Isolate'),
-        _vmService.streamListen('Debug'),
-      ]);
-
-      // Because we start paused, resume so the app is in a "running" state as
-      // expected by tests. Tests will reload/restart as required if they need
-      // to hit breakpoints, etc.
-      await waitForPause();
-      if (pauseOnExceptions) {
-        await _vmService.setExceptionPauseMode(await _getFlutterIsolateId(), ExceptionPauseMode.kUnhandled);
-      }
-      await resume(wait: false);
-    }
-
-    // Now await the started event; if it had already happened the future will
-    // have already completed.
-    _currentRunningAppId = (await started)['params']['appId'];
-  }
-
-  Future<void> hotRestart({bool pause = false}) => _restart(fullRestart: true, pause: pause);
-  Future<void> hotReload() => _restart(fullRestart: false);
-
-  Future<void> _restart({bool fullRestart = false, bool pause = false}) async {
-    if (_currentRunningAppId == null)
-      throw Exception('App has not started yet');
-
-    final dynamic hotReloadResp = await _sendRequest(
-        'app.restart',
-        <String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
-    );
-
-    if (hotReloadResp == null || hotReloadResp['code'] != 0)
-      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
-  }
-
-  Future<int> detach() async {
-    if (_vmService != null) {
-      _debugPrint('Closing VM service');
-      _vmService.dispose();
-    }
-    if (_currentRunningAppId != null) {
-      _debugPrint('Detaching from app');
-      await Future.any<void>(<Future<void>>[
-        _proc.exitCode,
-        _sendRequest(
-          'app.detach',
-          <String, dynamic>{'appId': _currentRunningAppId},
-        ),
-      ]).timeout(
-        quitTimeout,
-        onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
-      );
-      _currentRunningAppId = null;
-    }
-    _debugPrint('Waiting for process to end');
-    return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
-  }
-
-  Future<int> stop() async {
-    if (_vmService != null) {
-      _debugPrint('Closing VM service');
-      _vmService.dispose();
-    }
-    if (_currentRunningAppId != null) {
-      _debugPrint('Stopping app');
-      await Future.any<void>(<Future<void>>[
-        _proc.exitCode,
-        _sendRequest(
-          'app.stop',
-          <String, dynamic>{'appId': _currentRunningAppId},
-        ),
-      ]).timeout(
-        quitTimeout,
-        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
-      );
-      _currentRunningAppId = null;
-    }
-    if (_proc != null) {
-      _debugPrint('Waiting for process to end');
-      return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
-    }
-    return 0;
   }
 
   Future<int> quit() => _killGracefully();
@@ -309,20 +175,6 @@
     return wait ? waitForPause() : null;
   }
 
-  Future<Isolate> breakAt(Uri uri, int line, {bool restart = false}) async {
-    if (restart) {
-      // For a hot restart, we need to send the breakpoints after the restart
-      // so we need to pause during the restart to avoid races.
-      await hotRestart(pause: true);
-      await addBreakpoint(uri, line);
-      return resume();
-    } else {
-      await addBreakpoint(uri, line);
-      await hotReload();
-      return waitForPause();
-    }
-  }
-
   Future<InstanceRef> evaluateInFrame(String expression) async {
     return _timeoutWithMessages<InstanceRef>(
         () async => await _vmService.evaluateInFrame(await _getFlutterIsolateId(), 0, expression),
@@ -438,6 +290,175 @@
     }
     return null;
   }
+}
+
+class FlutterRunTestDriver extends FlutterTestDriver {
+  FlutterRunTestDriver(Directory _projectFolder, {String logPrefix}):
+    super(_projectFolder, logPrefix: logPrefix);
+
+  String _currentRunningAppId;
+
+   Future<void> run({
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
+    await _setupProcess(<String>[
+        'run',
+        '--machine',
+        '-d',
+        'flutter-tester',
+    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
+  }
+
+  Future<void> attach(
+    int port, {
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
+    await _setupProcess(<String>[
+        'attach',
+        '--machine',
+        '-d',
+        'flutter-tester',
+        '--debug-port',
+        '$port',
+    ], withDebugger: withDebugger, pauseOnExceptions: pauseOnExceptions, pidFile: pidFile);
+  }
+
+  @override
+  Future<void> _setupProcess(
+    List<String> args, {
+    bool withDebugger = false,
+    bool pauseOnExceptions = false,
+    File pidFile,
+  }) async {
+    await super._setupProcess(
+      args,
+      withDebugger: withDebugger,
+      pauseOnExceptions: pauseOnExceptions,
+      pidFile: pidFile,
+    );
+
+    // Stash the PID so that we can terminate the VM more reliably than using
+    // _proc.kill() (because _proc is a shell, because `flutter` is a shell
+    // script).
+    final Map<String, dynamic> connected = await _waitFor(event: 'daemon.connected');
+    _procPid = connected['params']['pid'];
+
+    // Set this up now, but we don't wait it yet. We want to make sure we don't
+    // miss it while waiting for debugPort below.
+    final Future<Map<String, dynamic>> started = _waitFor(event: 'app.started',
+        timeout: appStartTimeout);
+
+    if (withDebugger) {
+      final Map<String, dynamic> debugPort = await _waitFor(event: 'app.debugPort',
+          timeout: appStartTimeout);
+      final String wsUriString = debugPort['params']['wsUri'];
+      _vmServiceWsUri = Uri.parse(wsUriString);
+      _vmService =
+          await vmServiceConnectUri(_vmServiceWsUri.toString());
+      _vmService.onSend.listen((String s) => _debugPrint('==> $s'));
+      _vmService.onReceive.listen((String s) => _debugPrint('<== $s'));
+      await Future.wait(<Future<Success>>[
+        _vmService.streamListen('Isolate'),
+        _vmService.streamListen('Debug'),
+      ]);
+
+      // Because we start paused, resume so the app is in a "running" state as
+      // expected by tests. Tests will reload/restart as required if they need
+      // to hit breakpoints, etc.
+      await waitForPause();
+      if (pauseOnExceptions) {
+        await _vmService.setExceptionPauseMode(await _getFlutterIsolateId(), ExceptionPauseMode.kUnhandled);
+      }
+      await resume(wait: false);
+    }
+
+    // Now await the started event; if it had already happened the future will
+    // have already completed.
+    _currentRunningAppId = (await started)['params']['appId'];
+  }
+
+  Future<void> hotRestart({bool pause = false}) => _restart(fullRestart: true, pause: pause);
+  Future<void> hotReload() => _restart(fullRestart: false);
+
+  Future<void> _restart({bool fullRestart = false, bool pause = false}) async {
+    if (_currentRunningAppId == null)
+      throw Exception('App has not started yet');
+
+    final dynamic hotReloadResp = await _sendRequest(
+        'app.restart',
+        <String, dynamic>{'appId': _currentRunningAppId, 'fullRestart': fullRestart, 'pause': pause},
+    );
+
+    if (hotReloadResp == null || hotReloadResp['code'] != 0)
+      _throwErrorResponse('Hot ${fullRestart ? 'restart' : 'reload'} request failed');
+  }
+
+  Future<int> detach() async {
+    if (_vmService != null) {
+      _debugPrint('Closing VM service');
+      _vmService.dispose();
+    }
+    if (_currentRunningAppId != null) {
+      _debugPrint('Detaching from app');
+      await Future.any<void>(<Future<void>>[
+        _proc.exitCode,
+        _sendRequest(
+          'app.detach',
+          <String, dynamic>{'appId': _currentRunningAppId},
+        ),
+      ]).timeout(
+        quitTimeout,
+        onTimeout: () { _debugPrint('app.detach did not return within $quitTimeout'); },
+      );
+      _currentRunningAppId = null;
+    }
+    _debugPrint('Waiting for process to end');
+    return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
+  }
+
+  Future<int> stop() async {
+    if (_vmService != null) {
+      _debugPrint('Closing VM service');
+      _vmService.dispose();
+    }
+    if (_currentRunningAppId != null) {
+      _debugPrint('Stopping app');
+      await Future.any<void>(<Future<void>>[
+        _proc.exitCode,
+        _sendRequest(
+          'app.stop',
+          <String, dynamic>{'appId': _currentRunningAppId},
+        ),
+      ]).timeout(
+        quitTimeout,
+        onTimeout: () { _debugPrint('app.stop did not return within $quitTimeout'); },
+      );
+      _currentRunningAppId = null;
+    }
+    if (_proc != null) {
+      _debugPrint('Waiting for process to end');
+      return _proc.exitCode.timeout(quitTimeout, onTimeout: _killGracefully);
+    }
+    return 0;
+  }
+
+  Future<Isolate> breakAt(Uri uri, int line, { bool restart = false }) async {
+    if (restart) {
+      // For a hot restart, we need to send the breakpoints after the restart
+      // so we need to pause during the restart to avoid races.
+      await hotRestart(pause: true);
+      await addBreakpoint(uri, line);
+      return resume();
+    } else {
+      await addBreakpoint(uri, line);
+      await hotReload();
+      return waitForPause();
+    }
+  }
 
   int id = 1;
   Future<dynamic> _sendRequest(String method, dynamic params) async {