move reload and restart handling into terminal (#35846)


diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 94ab882..77c9e94 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -559,6 +559,9 @@
     });
   }
 
+  /// Whether this runner can hot reload.
+  bool get canHotReload => hotMode;
+
   /// Start the app and keep the process running during its lifetime.
   ///
   /// Returns the exit code that we should use for the flutter tool process; 0
@@ -838,35 +841,6 @@
 
   /// Called right before we exit.
   Future<void> cleanupAtFinish();
-
-  /// Called when the runner should handle a terminal command.
-  Future<void> handleTerminalCommand(String code) async {
-    switch (code) {
-      case 'r':
-        final OperationResult result = await restart(fullRestart: false);
-        if (!result.isOk) {
-          printStatus('Try again after fixing the above error(s).', emphasis: true);
-        }
-        return;
-      case 'R':
-        // If hot restart is not supported for all devices, ignore the command.
-        if (!canHotRestart) {
-          return;
-        }
-        final OperationResult result = await restart(fullRestart: true);
-        if (!result.isOk) {
-          printStatus('Try again after fixing the above error(s).', emphasis: true);
-        }
-        return;
-      case 'l':
-      case 'L':
-        final List<FlutterView> views = flutterDevices.expand((FlutterDevice d) => d.views).toList();
-        printStatus('Connected ${pluralize('view', views.length)}:');
-        for (FlutterView v in views) {
-          printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
-        }
-    }
-  }
 }
 
 class OperationResult {
@@ -918,6 +892,9 @@
   bool _processingUserRequest = false;
   StreamSubscription<void> subscription;
 
+  @visibleForTesting
+  String lastReceivedCommand;
+
   void setupTerminal() {
     if (!logger.quiet) {
       printStatus('');
@@ -964,6 +941,14 @@
           return true;
         }
         return false;
+      case 'l':
+        final List<FlutterView> views = residentRunner.flutterDevices
+            .expand((FlutterDevice d) => d.views).toList();
+        printStatus('Connected ${pluralize('view', views.length)}:');
+        for (FlutterView v in views) {
+          printStatus('${v.uiIsolate.name} (${v.uiIsolate.id})', indent: 2);
+        }
+        return true;
       case 'L':
         if (residentRunner.supportsServiceProtocol) {
           await residentRunner.debugDumpLayerTree();
@@ -1000,6 +985,25 @@
             await residentRunner.screenshot(device);
         }
         return true;
+      case 'r':
+        if (!residentRunner.canHotReload) {
+          return false;
+        }
+        final OperationResult result = await residentRunner.restart(fullRestart: false);
+        if (!result.isOk) {
+          printStatus('Try again after fixing the above error(s).', emphasis: true);
+        }
+        return true;
+      case 'R':
+        // If hot restart is not supported for all devices, ignore the command.
+        if (!residentRunner.canHotRestart || !residentRunner.hotMode) {
+          return false;
+        }
+        final OperationResult result = await residentRunner.restart(fullRestart: true);
+        if (!result.isOk) {
+          printStatus('Try again after fixing the above error(s).', emphasis: true);
+        }
+        return true;
       case 'S':
         if (residentRunner.supportsServiceProtocol) {
           await residentRunner.debugDumpSemanticsTreeInTraversalOrder();
@@ -1031,11 +1035,9 @@
         await residentRunner.debugToggleDebugCheckElevationsEnabled();
         return true;
     }
-
     return false;
   }
 
-
   Future<void> processTerminalInput(String command) async {
     // When terminal doesn't support line mode, '\n' can sneak into the input.
     command = command.trim();
@@ -1045,9 +1047,8 @@
     }
     _processingUserRequest = true;
     try {
-      final bool handled = await _commonTerminalInputHandler(command);
-      if (!handled)
-        await residentRunner.handleTerminalCommand(command);
+      lastReceivedCommand = command;
+      await _commonTerminalInputHandler(command);
     } catch (error, st) {
       printError('$error\n$st');
       await _cleanUpAndExit(null);
diff --git a/packages/flutter_tools/lib/src/resident_web_runner.dart b/packages/flutter_tools/lib/src/resident_web_runner.dart
index 55dad48..be37536 100644
--- a/packages/flutter_tools/lib/src/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_web_runner.dart
@@ -50,6 +50,9 @@
   final FlutterProject flutterProject;
 
   @override
+  bool get canHotReload => false;
+
+  @override
   Future<int> attach(
       {Completer<DebugConnectionInfo> connectionInfoCompleter,
       Completer<void> appStartedCompleter}) async {
@@ -74,17 +77,6 @@
   }
 
   @override
-  Future<void> handleTerminalCommand(String code) async {
-    if (code == 'R') {
-      // If hot restart is not supported for all devices, ignore the command.
-      if (!canHotRestart) {
-        return;
-      }
-      await restart(fullRestart: true);
-    }
-  }
-
-  @override
   void printHelp({bool details}) {
     const String fire = '🔥';
     const String rawMessage =
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index ad91f56..6a79afc 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -39,6 +39,12 @@
   bool _didAttach = false;
 
   @override
+  bool get canHotReload => false;
+
+  @override
+  bool get canHotRestart => false;
+
+  @override
   Future<int> run({
     Completer<DebugConnectionInfo> connectionInfoCompleter,
     Completer<void> appStartedCompleter,
diff --git a/packages/flutter_tools/test/terminal_handler_test.dart b/packages/flutter_tools/test/terminal_handler_test.dart
index 088c712..6e182ba 100644
--- a/packages/flutter_tools/test/terminal_handler_test.dart
+++ b/packages/flutter_tools/test/terminal_handler_test.dart
@@ -3,9 +3,12 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+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/globals.dart';
 import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/vmservice.dart';
 import 'package:mockito/mockito.dart';
 
 import 'src/common.dart';
@@ -24,37 +27,21 @@
     testUsingContext('single help character', () async {
       final TestRunner testRunner = createTestRunner();
       final TerminalHandler terminalHandler = TerminalHandler(testRunner);
-      expect(testRunner.hasHelpBeenPrinted, isFalse);
+      expect(testRunner.hasHelpBeenPrinted, false);
       await terminalHandler.processTerminalInput('h');
-      expect(testRunner.hasHelpBeenPrinted, isTrue);
+      expect(testRunner.hasHelpBeenPrinted, true);
     });
 
     testUsingContext('help character surrounded with newlines', () async {
       final TestRunner testRunner = createTestRunner();
       final TerminalHandler terminalHandler = TerminalHandler(testRunner);
-      expect(testRunner.hasHelpBeenPrinted, isFalse);
+      expect(testRunner.hasHelpBeenPrinted, false);
       await terminalHandler.processTerminalInput('\nh\n');
-      expect(testRunner.hasHelpBeenPrinted, isTrue);
-    });
-
-    testUsingContext('reload character with trailing newline', () async {
-      final TestRunner testRunner = createTestRunner();
-      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
-      expect(testRunner.receivedCommand, isNull);
-      await terminalHandler.processTerminalInput('r\n');
-      expect(testRunner.receivedCommand, equals('r'));
-    });
-
-    testUsingContext('newlines', () async {
-      final TestRunner testRunner = createTestRunner();
-      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
-      expect(testRunner.receivedCommand, isNull);
-      await terminalHandler.processTerminalInput('\n\n');
-      expect(testRunner.receivedCommand, equals(''));
+      expect(testRunner.hasHelpBeenPrinted, true);
     });
   });
 
-  group('keycode verification, brought to you by the letter r', () {
+  group('keycode verification, brought to you by the letter', () {
     MockResidentRunner mockResidentRunner;
     TerminalHandler terminalHandler;
 
@@ -62,7 +49,18 @@
       mockResidentRunner = MockResidentRunner();
       terminalHandler = TerminalHandler(mockResidentRunner);
       when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
-      when(mockResidentRunner.handleTerminalCommand(any)).thenReturn(null);
+    });
+
+    testUsingContext('a, can handle trailing newlines', () async {
+      await terminalHandler.processTerminalInput('a\n');
+
+      expect(terminalHandler.lastReceivedCommand, 'a');
+    });
+
+    testUsingContext('n, can handle trailing only newlines', () async {
+      await terminalHandler.processTerminalInput('\n\n');
+
+      expect(terminalHandler.lastReceivedCommand, '');
     });
 
     testUsingContext('a - debugToggleProfileWidgetBuilds with service protocol', () async {
@@ -116,6 +114,19 @@
       verifyNever(mockResidentRunner.debugToggleWidgetInspector());
     });
 
+    testUsingContext('l - list flutter views', () async {
+      final MockFlutterDevice mockFlutterDevice = MockFlutterDevice();
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      when(mockResidentRunner.flutterDevices).thenReturn(<FlutterDevice>[mockFlutterDevice]);
+      when(mockFlutterDevice.views).thenReturn(<FlutterView>[]);
+
+      await terminalHandler.processTerminalInput('l');
+
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText, contains('Connected views:\n'));
+    });
+
     testUsingContext('L - debugDumpLayerTree with service protocol', () async {
       await terminalHandler.processTerminalInput('L');
 
@@ -209,6 +220,74 @@
       verify(mockResidentRunner.screenshot(mockFlutterDevice)).called(1);
     });
 
+    testUsingContext('r - hotReload supported and succeeds', () async {
+      when(mockResidentRunner.canHotReload).thenReturn(true);
+      when(mockResidentRunner.restart(fullRestart: false))
+          .thenAnswer((Invocation invocation) async {
+            return OperationResult(0, '');
+          });
+      await terminalHandler.processTerminalInput('r');
+
+      verify(mockResidentRunner.restart(fullRestart: false)).called(1);
+    });
+
+    testUsingContext('r - hotReload supported and fails', () async {
+      when(mockResidentRunner.canHotReload).thenReturn(true);
+      when(mockResidentRunner.restart(fullRestart: false))
+          .thenAnswer((Invocation invocation) async {
+            return OperationResult(1, '');
+          });
+      await terminalHandler.processTerminalInput('r');
+
+      verify(mockResidentRunner.restart(fullRestart: false)).called(1);
+
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).'));
+    });
+
+    testUsingContext('r - hotReload unsupported', () async {
+      when(mockResidentRunner.canHotReload).thenReturn(false);
+      await terminalHandler.processTerminalInput('r');
+
+      verifyNever(mockResidentRunner.restart(fullRestart: false));
+    });
+
+    testUsingContext('R - hotRestart supported and succeeds', () async {
+      when(mockResidentRunner.canHotRestart).thenReturn(true);
+      when(mockResidentRunner.hotMode).thenReturn(true);
+      when(mockResidentRunner.restart(fullRestart: true))
+        .thenAnswer((Invocation invocation) async {
+          return OperationResult(0, '');
+        });
+      await terminalHandler.processTerminalInput('R');
+
+      verify(mockResidentRunner.restart(fullRestart: true)).called(1);
+    });
+
+    testUsingContext('R - hotRestart supported and fails', () async {
+      when(mockResidentRunner.canHotRestart).thenReturn(true);
+      when(mockResidentRunner.hotMode).thenReturn(true);
+      when(mockResidentRunner.restart(fullRestart: true))
+        .thenAnswer((Invocation invocation) async {
+          return OperationResult(1, 'fail');
+        });
+      await terminalHandler.processTerminalInput('R');
+
+      verify(mockResidentRunner.restart(fullRestart: true)).called(1);
+
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText, contains('Try again after fixing the above error(s).'));
+    });
+
+    testUsingContext('R - hot restart unsupported', () async {
+      when(mockResidentRunner.canHotRestart).thenReturn(false);
+      await terminalHandler.processTerminalInput('R');
+
+      verifyNever(mockResidentRunner.restart(fullRestart: true));
+    });
+
     testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder with service protocol', () async {
       await terminalHandler.processTerminalInput('S');
 
@@ -307,11 +386,6 @@
   Future<void> cleanupAtFinish() async { }
 
   @override
-  Future<void> handleTerminalCommand(String code) async {
-    receivedCommand = code;
-  }
-
-  @override
   void printHelp({ bool details }) {
     hasHelpBeenPrinted = true;
   }