[flutter_tools] macOS cleanups, attach to log reader in release mode (#61913)

diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 2c6499c..1cb415c 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -142,6 +142,10 @@
         config: globals.config,
         fuchsiaWorkflow: fuchsiaWorkflow,
         xcDevice: globals.xcdevice,
+        macOSWorkflow: MacOSWorkflow(
+          platform: globals.platform,
+          featureFlags: featureFlags,
+        ),
       ),
       Doctor: () => Doctor(logger: globals.logger),
       DoctorValidatorsProvider: () => DoctorValidatorsProvider.defaultInstance,
@@ -193,7 +197,10 @@
             outputPreferences: globals.outputPreferences,
             timeoutConfiguration: timeoutConfiguration,
           ),
-      MacOSWorkflow: () => const MacOSWorkflow(),
+      MacOSWorkflow: () => MacOSWorkflow(
+        featureFlags: featureFlags,
+        platform: globals.platform,
+      ),
       MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(),
       OperatingSystemUtils: () => OperatingSystemUtils(
         fileSystem: globals.fs,
diff --git a/packages/flutter_tools/lib/src/desktop_device.dart b/packages/flutter_tools/lib/src/desktop_device.dart
index 937bfcd..26561e5 100644
--- a/packages/flutter_tools/lib/src/desktop_device.dart
+++ b/packages/flutter_tools/lib/src/desktop_device.dart
@@ -5,10 +5,12 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
+import 'package:process/process.dart';
 
 import 'application_package.dart';
 import 'base/common.dart';
 import 'base/io.dart';
+import 'base/logger.dart';
 import 'build_info.dart';
 import 'convert.dart';
 import 'device.dart';
@@ -18,15 +20,23 @@
 /// A partial implementation of Device for desktop-class devices to inherit
 /// from, containing implementations that are common to all desktop devices.
 abstract class DesktopDevice extends Device {
-  DesktopDevice(String identifier, {@required PlatformType platformType, @required bool ephemeral}) : super(
-      identifier,
-      category: Category.desktop,
-      platformType: platformType,
-      ephemeral: ephemeral,
-  );
+  DesktopDevice(String identifier, {
+      @required PlatformType platformType,
+      @required bool ephemeral,
+      Logger logger,
+      ProcessManager processManager,
+    }) : _logger = logger ?? globals.logger, // TODO(jonahwilliams): remove after updating google3
+         _processManager = processManager ?? globals.processManager,
+         super(
+          identifier,
+          category: Category.desktop,
+          platformType: platformType,
+          ephemeral: ephemeral,
+        );
 
+  final Logger _logger;
+  final ProcessManager _processManager;
   final Set<Process> _runningProcesses = <Process>{};
-
   final DesktopLogReader _deviceLogReader = DesktopLogReader();
 
   // Since the host and target devices are the same, no work needs to be done
@@ -108,20 +118,20 @@
     final BuildMode buildMode = debuggingOptions?.buildInfo?.mode;
     final String executable = executablePathForDevice(package, buildMode);
     if (executable == null) {
-      globals.printError('Unable to find executable to run');
+      _logger.printError('Unable to find executable to run');
       return LaunchResult.failed();
     }
 
-    final Process process = await globals.processManager.start(<String>[
+    final Process process = await _processManager.start(<String>[
       executable,
     ]);
     _runningProcesses.add(process);
     unawaited(process.exitCode.then((_) => _runningProcesses.remove(process)));
 
+    _deviceLogReader.initializeProcess(process);
     if (debuggingOptions?.buildInfo?.isRelease == true) {
       return LaunchResult.succeeded();
     }
-    _deviceLogReader.initializeProcess(process);
     final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(_deviceLogReader,
       devicePort: debuggingOptions?.deviceVmServicePort,
       hostPort: debuggingOptions?.hostVmServicePort,
@@ -133,12 +143,12 @@
         onAttached(package, buildMode, process);
         return LaunchResult.succeeded(observatoryUri: observatoryUri);
       }
-      globals.printError(
+      _logger.printError(
         'Error waiting for a debug connection: '
         'The log reader stopped unexpectedly.',
       );
     } on Exception catch (error) {
-      globals.printError('Error waiting for a debug connection: $error');
+      _logger.printError('Error waiting for a debug connection: $error');
     } finally {
       await observatoryDiscovery.cancel();
     }
@@ -186,9 +196,7 @@
   void initializeProcess(Process process) {
     process.stdout.listen(_inputController.add);
     process.stderr.listen(_inputController.add);
-    process.exitCode.then((int result) {
-      _inputController.close();
-    });
+    process.exitCode.whenComplete(_inputController.close);
   }
 
   @override
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 2c72599..1130db6 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -33,6 +33,7 @@
 import 'ios/simulators.dart';
 import 'linux/linux_device.dart';
 import 'macos/macos_device.dart';
+import 'macos/macos_workflow.dart';
 import 'macos/xcode.dart';
 import 'project.dart';
 import 'tester/flutter_tester.dart';
@@ -292,6 +293,7 @@
     @required FlutterVersion flutterVersion,
     @required Config config,
     @required Artifacts artifacts,
+    @required MacOSWorkflow macOSWorkflow,
   }) : deviceDiscoverers =  <DeviceDiscovery>[
     AndroidDevices(
       logger: logger,
@@ -322,10 +324,17 @@
       logger: logger,
       artifacts: artifacts,
     ),
-    MacOSDevices(),
+    MacOSDevices(
+      processManager: processManager,
+      macOSWorkflow: macOSWorkflow,
+      logger: logger,
+      platform: platform,
+    ),
     LinuxDevices(
       platform: platform,
       featureFlags: featureFlags,
+      processManager: processManager,
+      logger: logger,
     ),
     WindowsDevices(),
     WebDevices(
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 389068e..8c40420 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -65,6 +65,11 @@
     featureFlags: featureFlags,
   );
 
+  final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
+    platform: globals.platform,
+    featureFlags: featureFlags,
+  );
+
   @override
   List<DoctorValidator> get validators {
     if (_validators != null) {
diff --git a/packages/flutter_tools/lib/src/linux/linux_device.dart b/packages/flutter_tools/lib/src/linux/linux_device.dart
index 2cc4295..8916f46 100644
--- a/packages/flutter_tools/lib/src/linux/linux_device.dart
+++ b/packages/flutter_tools/lib/src/linux/linux_device.dart
@@ -3,12 +3,15 @@
 // found in the LICENSE file.
 
 import 'package:meta/meta.dart';
+import 'package:process/process.dart';
 
+import '../base/logger.dart';
 import '../base/platform.dart';
 import '../build_info.dart';
 import '../desktop_device.dart';
 import '../device.dart';
 import '../features.dart';
+import '../globals.dart' as globals;
 import '../project.dart';
 import 'application_package.dart';
 import 'build_linux.dart';
@@ -16,10 +19,15 @@
 
 /// A device that represents a desktop Linux target.
 class LinuxDevice extends DesktopDevice {
-  LinuxDevice() : super(
+  LinuxDevice({
+    @required ProcessManager processManager,
+    @required Logger logger,
+  }) : super(
       'linux',
       platformType: PlatformType.linux,
       ephemeral: false,
+      logger: logger,
+      processManager: processManager,
   );
 
   @override
@@ -59,15 +67,21 @@
   LinuxDevices({
     @required Platform platform,
     @required FeatureFlags featureFlags,
-  }) : _platform = platform,
+    ProcessManager processManager,
+    Logger logger,
+  }) : _platform = platform ?? globals.platform, // TODO(jonahwilliams): remove after google3 roll
        _linuxWorkflow = LinuxWorkflow(
           platform: platform,
           featureFlags: featureFlags,
        ),
+       _logger = logger,
+       _processManager = processManager ?? globals.processManager,
        super('linux devices');
 
   final Platform _platform;
   final LinuxWorkflow _linuxWorkflow;
+  final ProcessManager _processManager;
+  final Logger _logger;
 
   @override
   bool get supportsPlatform => _platform.isLinux;
@@ -81,7 +95,10 @@
       return const <Device>[];
     }
     return <Device>[
-      LinuxDevice(),
+      LinuxDevice(
+        logger: _logger,
+        processManager: _processManager,
+      ),
     ];
   }
 
diff --git a/packages/flutter_tools/lib/src/macos/macos_device.dart b/packages/flutter_tools/lib/src/macos/macos_device.dart
index 1d7ef01..f92b7f3 100644
--- a/packages/flutter_tools/lib/src/macos/macos_device.dart
+++ b/packages/flutter_tools/lib/src/macos/macos_device.dart
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
 import '../base/io.dart';
+import '../base/logger.dart';
+import '../base/platform.dart';
 import '../build_info.dart';
 import '../desktop_device.dart';
 import '../device.dart';
-import '../globals.dart' as globals;
 import '../macos/application_package.dart';
 import '../project.dart';
 import 'build_macos.dart';
@@ -14,11 +18,21 @@
 
 /// A device that represents a desktop MacOS target.
 class MacOSDevice extends DesktopDevice {
-  MacOSDevice() : super(
-      'macos',
-      platformType: PlatformType.macos,
-      ephemeral: false,
-  );
+  MacOSDevice({
+    @required ProcessManager processManager,
+    @required Logger logger,
+  }) : _processManager = processManager,
+       _logger = logger,
+       super(
+        'macos',
+        platformType: PlatformType.macos,
+        ephemeral: false,
+        processManager: processManager,
+        logger: logger,
+      );
+
+  final ProcessManager _processManager;
+  final Logger _logger;
 
   @override
   bool isSupported() => true;
@@ -44,7 +58,7 @@
       flutterProject: FlutterProject.current(),
       buildInfo: buildInfo,
       targetOverride: mainPath,
-      verboseLogging: globals.logger.isVerbose,
+      verboseLogging: _logger.isVerbose,
     );
   }
 
@@ -59,7 +73,7 @@
     // than post-attach, since this won't run for release builds, but there's
     // no general-purpose way of knowing when a process is far enoug along in
     // the launch process for 'open' to foreground it.
-    globals.processManager.run(<String>[
+    _processManager.run(<String>[
       'open', package.applicationBundle(buildMode),
     ]).then((ProcessResult result) {
       if (result.exitCode != 0) {
@@ -70,13 +84,27 @@
 }
 
 class MacOSDevices extends PollingDeviceDiscovery {
-  MacOSDevices() : super('macOS devices');
+  MacOSDevices({
+    @required Platform platform,
+    @required MacOSWorkflow macOSWorkflow,
+    @required ProcessManager processManager,
+    @required Logger logger,
+  }) : _logger = logger,
+       _platform = platform,
+       _macOSWorkflow = macOSWorkflow,
+       _processManager = processManager,
+       super('macOS devices');
+
+  final MacOSWorkflow _macOSWorkflow;
+  final Platform _platform;
+  final ProcessManager _processManager;
+  final Logger _logger;
 
   @override
-  bool get supportsPlatform => globals.platform.isMacOS;
+  bool get supportsPlatform => _platform.isMacOS;
 
   @override
-  bool get canListAnything => macOSWorkflow.canListDevices;
+  bool get canListAnything => _macOSWorkflow.canListDevices;
 
   @override
   Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
@@ -84,7 +112,7 @@
       return const <Device>[];
     }
     return <Device>[
-      MacOSDevice(),
+      MacOSDevice(processManager: _processManager, logger: _logger),
     ];
   }
 
diff --git a/packages/flutter_tools/lib/src/macos/macos_workflow.dart b/packages/flutter_tools/lib/src/macos/macos_workflow.dart
index 0ae1ae4..b91c68c 100644
--- a/packages/flutter_tools/lib/src/macos/macos_workflow.dart
+++ b/packages/flutter_tools/lib/src/macos/macos_workflow.dart
@@ -2,29 +2,35 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import '../base/context.dart';
+import 'package:meta/meta.dart';
+
+import '../base/platform.dart';
 import '../doctor.dart';
 import '../features.dart';
-import '../globals.dart' as globals;
 
-/// The [MacOSWorkflow] instance.
-MacOSWorkflow get macOSWorkflow => context.get<MacOSWorkflow>();
 
 /// The macOS-specific implementation of a [Workflow].
 ///
 /// This workflow requires the flutter-desktop-embedding as a sibling
 /// repository to the flutter repo.
 class MacOSWorkflow implements Workflow {
-  const MacOSWorkflow();
+  const MacOSWorkflow({
+    @required Platform platform,
+    @required FeatureFlags featureFlags,
+  }) : _platform = platform,
+       _featureFlags = featureFlags;
+
+  final Platform _platform;
+  final FeatureFlags _featureFlags;
 
   @override
-  bool get appliesToHostPlatform => globals.platform.isMacOS && featureFlags.isMacOSEnabled;
+  bool get appliesToHostPlatform => _platform.isMacOS && _featureFlags.isMacOSEnabled;
 
   @override
-  bool get canLaunchDevices => globals.platform.isMacOS && featureFlags.isMacOSEnabled;
+  bool get canLaunchDevices => _platform.isMacOS && _featureFlags.isMacOSEnabled;
 
   @override
-  bool get canListDevices => globals.platform.isMacOS && featureFlags.isMacOSEnabled;
+  bool get canListDevices => _platform.isMacOS && _featureFlags.isMacOSEnabled;
 
   @override
   bool get canListEmulators => false;
diff --git a/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart b/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart
index 72ec9f7..5a8e458 100644
--- a/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/device.dart';
@@ -17,15 +18,21 @@
 import '../../src/context.dart';
 import '../../src/testbed.dart';
 
-void main() {
-  final LinuxDevice device = LinuxDevice();
-  final MockPlatform notLinux = MockPlatform();
-  when(notLinux.isLinux).thenReturn(false);
+final FakePlatform linux = FakePlatform(
+  operatingSystem: 'linux',
+);
+final FakePlatform windows = FakePlatform(
+  operatingSystem: 'windows',
+);
 
-  final MockPlatform mockLinuxPlatform = MockPlatform();
-  when(mockLinuxPlatform.isLinux).thenReturn(true);
+void main() {
 
   testWithoutContext('LinuxDevice defaults', () async {
+    final LinuxDevice device = LinuxDevice(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+    );
+
     final PrebuiltLinuxApp linuxApp = PrebuiltLinuxApp(executable: 'foo');
     expect(await device.targetPlatform, TargetPlatform.linux_x64);
     expect(device.name, 'Linux');
@@ -44,30 +51,38 @@
 
   testWithoutContext('LinuxDevice: no devices listed if platform unsupported', () async {
     expect(await LinuxDevices(
-      platform: notLinux,
+      platform: windows,
       featureFlags: TestFeatureFlags(isLinuxEnabled: true),
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
     ).devices, <Device>[]);
   });
 
   testWithoutContext('LinuxDevice: no devices listed if Linux feature flag disabled', () async {
     expect(await LinuxDevices(
-      platform: mockLinuxPlatform,
+      platform: linux,
       featureFlags: TestFeatureFlags(isLinuxEnabled: false),
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
     ).devices, <Device>[]);
   });
 
   testWithoutContext('LinuxDevice: devices', () async {
     expect(await LinuxDevices(
-      platform: mockLinuxPlatform,
+      platform: linux,
       featureFlags: TestFeatureFlags(isLinuxEnabled: true),
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
     ).devices, hasLength(1));
   });
 
   testWithoutContext('LinuxDevice: discoverDevices', () async {
     // Timeout ignored.
     final List<Device> devices = await LinuxDevices(
-      platform: mockLinuxPlatform,
+      platform: linux,
       featureFlags: TestFeatureFlags(isLinuxEnabled: true),
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
     ).discoverDevices(timeout: const Duration(seconds: 10));
     expect(devices, hasLength(1));
   });
@@ -78,7 +93,10 @@
     globals.fs.directory('linux').createSync();
     final FlutterProject flutterProject = FlutterProject.current();
 
-    expect(LinuxDevice().isSupportedForProject(flutterProject), true);
+    expect(LinuxDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    ).isSupportedForProject(flutterProject), true);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
     ProcessManager: () => FakeProcessManager.any(),
@@ -89,7 +107,10 @@
     globals.fs.file('.packages').createSync();
     final FlutterProject flutterProject = FlutterProject.current();
 
-    expect(LinuxDevice().isSupportedForProject(flutterProject), false);
+    expect(LinuxDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    ).isSupportedForProject(flutterProject), false);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
     ProcessManager: () => FakeProcessManager.any(),
@@ -97,6 +118,10 @@
 
   testUsingContext('LinuxDevice.executablePathForDevice uses the correct package executable', () async {
     final MockLinuxApp mockApp = MockLinuxApp();
+    final LinuxDevice device = LinuxDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    );
     const String debugPath = 'debug/executable';
     const String profilePath = 'profile/executable';
     const String releasePath = 'release/executable';
@@ -104,15 +129,13 @@
     when(mockApp.executable(BuildMode.profile)).thenReturn(profilePath);
     when(mockApp.executable(BuildMode.release)).thenReturn(releasePath);
 
-    expect(LinuxDevice().executablePathForDevice(mockApp, BuildMode.debug), debugPath);
-    expect(LinuxDevice().executablePathForDevice(mockApp, BuildMode.profile), profilePath);
-    expect(LinuxDevice().executablePathForDevice(mockApp, BuildMode.release), releasePath);
+    expect(device.executablePathForDevice(mockApp, BuildMode.debug), debugPath);
+    expect(device.executablePathForDevice(mockApp, BuildMode.profile), profilePath);
+    expect(device.executablePathForDevice(mockApp, BuildMode.release), releasePath);
   }, overrides: <Type, Generator>{
     FileSystem: () => MemoryFileSystem(),
     ProcessManager: () => FakeProcessManager.any(),
   });
 }
 
-class MockPlatform extends Mock implements Platform {}
-
 class MockLinuxApp extends Mock implements LinuxApp {}
diff --git a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
index 728a596..0b0b66e 100644
--- a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
@@ -2,17 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/context.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/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/device.dart';
-import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
 import 'package:flutter_tools/src/macos/application_package.dart';
 import 'package:flutter_tools/src/macos/macos_device.dart';
+import 'package:flutter_tools/src/macos/macos_workflow.dart';
 import 'package:flutter_tools/src/project.dart';
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
@@ -21,104 +23,178 @@
 import '../../src/context.dart';
 import '../../src/testbed.dart';
 
+final FakePlatform macOS = FakePlatform(
+  operatingSystem: 'macos',
+);
+
+final FakePlatform linux = FakePlatform(
+  operatingSystem: 'linux',
+);
+
 void main() {
-  group(MacOSDevice, () {
-    final MacOSDevice device = MacOSDevice();
-    final MockProcessManager mockProcessManager = MockProcessManager();
+  testWithoutContext('default configuration', () async {
+    final MacOSDevice device = MacOSDevice(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+    );
+    final MockMacOSApp mockMacOSApp = MockMacOSApp();
 
-    final MockPlatform notMac = MockPlatform();
-    when(notMac.isMacOS).thenReturn(false);
-    when(notMac.environment).thenReturn(const <String, String>{});
+    expect(await device.targetPlatform, TargetPlatform.darwin_x64);
+    expect(device.name, 'macOS');
+    expect(await device.installApp(mockMacOSApp), true);
+    expect(await device.uninstallApp(mockMacOSApp), true);
+    expect(await device.isLatestBuildInstalled(mockMacOSApp), true);
+    expect(await device.isAppInstalled(mockMacOSApp), true);
+    expect(device.category, Category.desktop);
 
-    final MockPlatform mockMacPlatform = MockPlatform();
-    when(mockMacPlatform.isMacOS).thenReturn(true);
+    expect(device.supportsRuntimeMode(BuildMode.debug), true);
+    expect(device.supportsRuntimeMode(BuildMode.profile), true);
+    expect(device.supportsRuntimeMode(BuildMode.release), true);
+    expect(device.supportsRuntimeMode(BuildMode.jitRelease), false);
+  });
 
-    when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
-      return ProcessResult(0, 1, '', '');
-    });
+  testUsingContext('Attaches to log reader when running in release mode', () async {
+    final Completer<void> completer = Completer<void>();
+    final MacOSDevice device = MacOSDevice(
+      processManager: FakeProcessManager.list(<FakeCommand>[
+        FakeCommand(
+          command: const <String>['Example.app'],
+          stdout: 'Hello World',
+          stderr: 'Goodnight, Moon',
+          completer: completer,
+        )
+      ]),
+      logger: BufferLogger.test(),
+    );
+    final MockMacOSApp mockMacOSApp = MockMacOSApp();
+    when(mockMacOSApp.executable(BuildMode.release)).thenReturn('Example.app');
 
-    testUsingContext('defaults', () async {
-      final MockMacOSApp mockMacOSApp = MockMacOSApp();
-      expect(await device.targetPlatform, TargetPlatform.darwin_x64);
-      expect(device.name, 'macOS');
-      expect(await device.installApp(mockMacOSApp), true);
-      expect(await device.uninstallApp(mockMacOSApp), true);
-      expect(await device.isLatestBuildInstalled(mockMacOSApp), true);
-      expect(await device.isAppInstalled(mockMacOSApp), true);
-      expect(device.category, Category.desktop);
+    final LaunchResult result = await device.startApp(
+      mockMacOSApp,
+      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+      prebuiltApplication: true,
+    );
 
-      expect(device.supportsRuntimeMode(BuildMode.debug), true);
-      expect(device.supportsRuntimeMode(BuildMode.profile), true);
-      expect(device.supportsRuntimeMode(BuildMode.release), true);
-      expect(device.supportsRuntimeMode(BuildMode.jitRelease), false);
-    });
+    expect(result.started, true);
 
-    testUsingContext('No devices listed if platform unsupported', () async {
-      expect(await MacOSDevices().devices, <Device>[]);
-    }, overrides: <Type, Generator>{
-      Platform: () => notMac,
-    });
+    final DeviceLogReader logReader = device.getLogReader(app: mockMacOSApp);
 
-    testUsingContext('devices', () async {
-      expect(await MacOSDevices().devices, hasLength(1));
-    }, overrides: <Type, Generator>{
-      Platform: () => mockMacPlatform,
-      FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
-    });
+    expect(logReader.logLines, emits('Hello WorldGoodnight, Moon'));
+    completer.complete();
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
 
-    testUsingContext('discoverDevices', () async {
-      // Timeout ignored.
-      final List<Device> devices = await MacOSDevices().discoverDevices(timeout: const Duration(seconds: 10));
-      expect(devices, hasLength(1));
-    }, overrides: <Type, Generator>{
-      Platform: () => mockMacPlatform,
-      FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
-    });
+  testWithoutContext('No devices listed if platform is unsupported', () async {
+    expect(await MacOSDevices(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+      platform: linux,
+      macOSWorkflow: MacOSWorkflow(
+        featureFlags: TestFeatureFlags(isMacOSEnabled: true),
+        platform: linux,
+      ),
+    ).devices, isEmpty);
+  });
 
-    testUsingContext('isSupportedForProject is true with editable host app', () async {
-      globals.fs.file('pubspec.yaml').createSync();
-      globals.fs.file('.packages').createSync();
-      globals.fs.directory('macos').createSync();
-      final FlutterProject flutterProject = FlutterProject.current();
+  testWithoutContext('No devices listed if platform is supported and feature is disabled', () async {
+    final MacOSDevices macOSDevices = MacOSDevices(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+      platform: macOS,
+      macOSWorkflow: MacOSWorkflow(
+        featureFlags: TestFeatureFlags(isMacOSEnabled: false),
+        platform: macOS,
+      ),
+    );
 
-      expect(MacOSDevice().isSupportedForProject(flutterProject), true);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
+    expect(await macOSDevices.devices, isEmpty);
+  });
 
-    testUsingContext('isSupportedForProject is false with no host app', () async {
-      globals.fs.file('pubspec.yaml').createSync();
-      globals.fs.file('.packages').createSync();
-      final FlutterProject flutterProject = FlutterProject.current();
+  testWithoutContext('devices listed if platform is supported and feature is enabled', () async {
+    final MacOSDevices macOSDevices = MacOSDevices(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+      platform: macOS,
+      macOSWorkflow: MacOSWorkflow(
+        featureFlags: TestFeatureFlags(isMacOSEnabled: true),
+        platform: macOS,
+      ),
+    );
 
-      expect(MacOSDevice().isSupportedForProject(flutterProject), false);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
+    expect(await macOSDevices.devices, hasLength(1));
+  });
 
-    testUsingContext('executablePathForDevice uses the correct package executable', () async {
-      final MockMacOSApp mockApp = MockMacOSApp();
-      const String debugPath = 'debug/executable';
-      const String profilePath = 'profile/executable';
-      const String releasePath = 'release/executable';
-      when(mockApp.executable(BuildMode.debug)).thenReturn(debugPath);
-      when(mockApp.executable(BuildMode.profile)).thenReturn(profilePath);
-      when(mockApp.executable(BuildMode.release)).thenReturn(releasePath);
+  testWithoutContext('can discover devices with a provided timeout', () async {
+    final MacOSDevices macOSDevices = MacOSDevices(
+      processManager: FakeProcessManager.any(),
+      logger: BufferLogger.test(),
+      platform: macOS,
+      macOSWorkflow: MacOSWorkflow(
+        featureFlags: TestFeatureFlags(isMacOSEnabled: true),
+        platform: macOS,
+      ),
+    );
 
-      expect(MacOSDevice().executablePathForDevice(mockApp, BuildMode.debug), debugPath);
-      expect(MacOSDevice().executablePathForDevice(mockApp, BuildMode.profile), profilePath);
-      expect(MacOSDevice().executablePathForDevice(mockApp, BuildMode.release), releasePath);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
+    // Timeout ignored.
+    final List<Device> devices = await macOSDevices.discoverDevices(timeout: const Duration(seconds: 10));
+
+    expect(devices, hasLength(1));
+  });
+
+  testUsingContext('isSupportedForProject is true with editable host app', () async {
+    final MacOSDevice device = MacOSDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    );
+
+    globals.fs.file('pubspec.yaml').createSync();
+    globals.fs.file('.packages').createSync();
+    globals.fs.directory('macos').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(device.isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('isSupportedForProject is false with no host app', () async {
+    final MacOSDevice device = MacOSDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    );
+    globals.fs.file('pubspec.yaml').createSync();
+    globals.fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(device.isSupportedForProject(flutterProject), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('executablePathForDevice uses the correct package executable', () async {
+    final MockMacOSApp mockApp = MockMacOSApp();
+    final MacOSDevice device = MacOSDevice(
+      logger: BufferLogger.test(),
+      processManager: FakeProcessManager.any(),
+    );
+    const String debugPath = 'debug/executable';
+    const String profilePath = 'profile/executable';
+    const String releasePath = 'release/executable';
+    when(mockApp.executable(BuildMode.debug)).thenReturn(debugPath);
+    when(mockApp.executable(BuildMode.profile)).thenReturn(profilePath);
+    when(mockApp.executable(BuildMode.release)).thenReturn(releasePath);
+
+    expect(device.executablePathForDevice(mockApp, BuildMode.debug), debugPath);
+    expect(device.executablePathForDevice(mockApp, BuildMode.profile), profilePath);
+    expect(device.executablePathForDevice(mockApp, BuildMode.release), releasePath);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
   });
 }
 
-class MockPlatform extends Mock implements Platform {}
-
 class MockMacOSApp extends Mock implements MacOSApp {}
-
-class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/macos/macos_workflow_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_workflow_test.dart
index 0c598de..37e4d30 100644
--- a/packages/flutter_tools/test/general.shard/macos/macos_workflow_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/macos_workflow_test.dart
@@ -3,60 +3,53 @@
 // found in the LICENSE file.
 
 import 'package:flutter_tools/src/base/platform.dart';
-import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/macos/macos_workflow.dart';
-import 'package:mockito/mockito.dart';
-import 'package:process/process.dart';
 
 import '../../src/common.dart';
-import '../../src/context.dart';
 import '../../src/testbed.dart';
 
+final FakePlatform macOS = FakePlatform(
+  operatingSystem: 'macos',
+);
+
+final FakePlatform linux = FakePlatform(
+  operatingSystem: 'linux',
+);
+
 void main() {
-  MockPlatform mac;
-  MockPlatform notMac;
-  Testbed testbed;
+  testWithoutContext('Applies to macOS platform', () {
+    final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
+      platform: macOS,
+      featureFlags: TestFeatureFlags(isMacOSEnabled: true),
+    );
 
-  setUp(() {
-    mac = MockPlatform();
-    notMac = MockPlatform();
-    when(mac.isMacOS).thenReturn(true);
-    when(notMac.isMacOS).thenReturn(false);
-    testbed = Testbed(overrides: <Type, Generator>{
-      Platform: () => mac,
-      FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
-    });
-  });
-
-  test('Applies to macOS platform', () => testbed.run(() {
     expect(macOSWorkflow.appliesToHostPlatform, true);
     expect(macOSWorkflow.canListDevices, true);
     expect(macOSWorkflow.canLaunchDevices, true);
     expect(macOSWorkflow.canListEmulators, false);
-  }));
+  });
 
-  test('Does not apply to non-macOS platform', () => testbed.run(() {
+  testWithoutContext('Does not apply to non-macOS platform', () {
+    final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
+      platform: linux,
+      featureFlags: TestFeatureFlags(isMacOSEnabled: true),
+    );
+
     expect(macOSWorkflow.appliesToHostPlatform, false);
     expect(macOSWorkflow.canListDevices, false);
     expect(macOSWorkflow.canLaunchDevices, false);
     expect(macOSWorkflow.canListEmulators, false);
-  }, overrides: <Type, Generator>{
-    Platform: () => notMac,
-  }));
+  });
 
-  test('Does not apply when feature is disabled', () => testbed.run(() {
+  testWithoutContext('Does not apply when feature is disabled', () {
+    final MacOSWorkflow macOSWorkflow = MacOSWorkflow(
+      platform: macOS,
+      featureFlags: TestFeatureFlags(isMacOSEnabled: false),
+    );
+
     expect(macOSWorkflow.appliesToHostPlatform, false);
     expect(macOSWorkflow.canListDevices, false);
     expect(macOSWorkflow.canLaunchDevices, false);
     expect(macOSWorkflow.canListEmulators, false);
-  }, overrides: <Type, Generator>{
-    FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: false),
-  }));
+  });
 }
-
-class MockPlatform extends Mock implements Platform {
-  @override
-  Map<String, String> environment = <String, String>{};
-}
-
-class MockProcessManager extends Mock implements ProcessManager {}