Move tools tests into a general.shard directory in preparation to changing how we shard tools tests (#36108)

diff --git a/packages/flutter_tools/test/general.shard/analytics_test.dart b/packages/flutter_tools/test/general.shard/analytics_test.dart
new file mode 100644
index 0000000..4c4cd53
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/analytics_test.dart
@@ -0,0 +1,197 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/time.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/commands/config.dart';
+import 'package:flutter_tools/src/commands/doctor.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:flutter_tools/src/version.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('analytics', () {
+    Directory tempDir;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      Cache.flutterRoot = '../..';
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    // Ensure we don't send anything when analytics is disabled.
+    testUsingContext('doesn\'t send when disabled', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+
+      flutterUsage.enabled = false;
+      await createProject(tempDir);
+      expect(count, 0);
+
+      flutterUsage.enabled = true;
+      await createProject(tempDir);
+      expect(count, flutterUsage.isFirstRun ? 0 : 2);
+
+      count = 0;
+      flutterUsage.enabled = false;
+      final DoctorCommand doctorCommand = DoctorCommand();
+      final CommandRunner<void>runner = createTestCommandRunner(doctorCommand);
+      await runner.run(<String>['doctor']);
+      expect(count, 0);
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => FlutterVersion(const SystemClock()),
+      Usage: () => Usage(configDirOverride: tempDir.path),
+    });
+
+    // Ensure we don't send for the 'flutter config' command.
+    testUsingContext('config doesn\'t send', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+
+      flutterUsage.enabled = false;
+      final ConfigCommand command = ConfigCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['config']);
+      expect(count, 0);
+
+      flutterUsage.enabled = true;
+      await runner.run(<String>['config']);
+      expect(count, 0);
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => FlutterVersion(const SystemClock()),
+      Usage: () => Usage(configDirOverride: tempDir.path),
+    });
+  });
+
+  group('analytics with mocks', () {
+    Usage mockUsage;
+    SystemClock mockClock;
+    Doctor mockDoctor;
+    List<int> mockTimes;
+
+    setUp(() {
+      mockUsage = MockUsage();
+      when(mockUsage.isFirstRun).thenReturn(false);
+      mockClock = MockClock();
+      mockDoctor = MockDoctor();
+      when(mockClock.now()).thenAnswer(
+        (Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
+      );
+    });
+
+    testUsingContext('flutter commands send timing events', () async {
+      mockTimes = <int>[1000, 2000];
+      when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => true);
+      final DoctorCommand command = DoctorCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['doctor']);
+
+      verify(mockClock.now()).called(2);
+
+      expect(
+        verify(mockUsage.sendTiming(captureAny, captureAny, captureAny, label: captureAnyNamed('label'))).captured,
+        <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'success'],
+      );
+    }, overrides: <Type, Generator>{
+      SystemClock: () => mockClock,
+      Doctor: () => mockDoctor,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('doctor fail sends warning', () async {
+      mockTimes = <int>[1000, 2000];
+      when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => false);
+      final DoctorCommand command = DoctorCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['doctor']);
+
+      verify(mockClock.now()).called(2);
+
+      expect(
+        verify(mockUsage.sendTiming(captureAny, captureAny, captureAny, label: captureAnyNamed('label'))).captured,
+        <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'warning'],
+      );
+    }, overrides: <Type, Generator>{
+      SystemClock: () => mockClock,
+      Doctor: () => mockDoctor,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('single command usage path', () async {
+      final FlutterCommand doctorCommand = DoctorCommand();
+      expect(await doctorCommand.usagePath, 'doctor');
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('compound command usage path', () async {
+      final BuildCommand buildCommand = BuildCommand();
+      final FlutterCommand buildApkCommand = buildCommand.subcommands['apk'];
+      expect(await buildApkCommand.usagePath, 'build/apk');
+    }, overrides: <Type, Generator>{
+      Usage: () => mockUsage,
+    });
+  });
+
+  group('analytics bots', () {
+    Directory tempDir;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('don\'t send on bots', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+
+      await createTestCommandRunner().run(<String>['--version']);
+      expect(count, 0);
+    }, overrides: <Type, Generator>{
+      Usage: () => Usage(
+        settingsName: 'flutter_bot_test',
+        versionOverride: 'dev/unknown',
+        configDirOverride: tempDir.path,
+      ),
+    });
+
+    testUsingContext('don\'t send on bots even when opted in', () async {
+      int count = 0;
+      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
+      flutterUsage.enabled = true;
+
+      await createTestCommandRunner().run(<String>['--version']);
+      expect(count, 0);
+    }, overrides: <Type, Generator>{
+      Usage: () => Usage(
+        settingsName: 'flutter_bot_test',
+        versionOverride: 'dev/unknown',
+        configDirOverride: tempDir.path,
+      ),
+    });
+  });
+}
+
+class MockUsage extends Mock implements Usage {}
+
+class MockDoctor extends Mock implements Doctor {}
diff --git a/packages/flutter_tools/test/general.shard/android/android_device_test.dart b/packages/flutter_tools/test/general.shard/android/android_device_test.dart
new file mode 100644
index 0000000..c439573
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_device_test.dart
@@ -0,0 +1,603 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/android_console.dart';
+import 'package:flutter_tools/src/android/android_device.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('android_device', () {
+    testUsingContext('stores the requested id', () {
+      const String deviceId = '1234';
+      final AndroidDevice device = AndroidDevice(deviceId);
+      expect(device.id, deviceId);
+    });
+  });
+
+  group('getAdbDevices', () {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    testUsingContext('throws on missing adb path', () {
+      final Directory sdkDir = MockAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final File adbExe = fs.file(getAdbPath(androidSdk));
+      when(mockProcessManager.runSync(
+        <String>[adbExe.path, 'devices', '-l'],
+      ))
+      .thenAnswer(
+        (_) => throw ArgumentError(adbExe.path),
+      );
+      expect(() => getAdbDevices(), throwsToolExit(message: RegExp('Unable to run "adb".*${adbExe.path}')));
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => MockAndroidSdk(),
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('physical devices', () {
+      final List<AndroidDevice> devices = <AndroidDevice>[];
+      parseADBDeviceOutput('''
+List of devices attached
+05a02bac               device usb:336592896X product:razor model:Nexus_7 device:flo
+
+''', devices: devices);
+      expect(devices, hasLength(1));
+      expect(devices.first.name, 'Nexus 7');
+      expect(devices.first.category, Category.mobile);
+    });
+
+    testUsingContext('emulators and short listings', () {
+      final List<AndroidDevice> devices = <AndroidDevice>[];
+      parseADBDeviceOutput('''
+List of devices attached
+localhost:36790        device
+0149947A0D01500C       device usb:340787200X
+emulator-5612          host features:shell_2
+
+''', devices: devices);
+      expect(devices, hasLength(3));
+      expect(devices.first.name, 'localhost:36790');
+    });
+
+    testUsingContext('android n', () {
+      final List<AndroidDevice> devices = <AndroidDevice>[];
+      parseADBDeviceOutput('''
+List of devices attached
+ZX1G22JJWR             device usb:3-3 product:shamu model:Nexus_6 device:shamu features:cmd,shell_v2
+''', devices: devices);
+      expect(devices, hasLength(1));
+      expect(devices.first.name, 'Nexus 6');
+    });
+
+    testUsingContext('adb error message', () {
+      final List<AndroidDevice> devices = <AndroidDevice>[];
+      final List<String> diagnostics = <String>[];
+      parseADBDeviceOutput('''
+It appears you do not have 'Android SDK Platform-tools' installed.
+Use the 'android' tool to install them:
+    android update sdk --no-ui --filter 'platform-tools'
+''', devices: devices, diagnostics: diagnostics);
+      expect(devices, hasLength(0));
+      expect(diagnostics, hasLength(1));
+      expect(diagnostics.first, contains('you do not have'));
+    });
+  });
+
+  group('parseAdbDeviceProperties', () {
+    test('parse adb shell output', () {
+      final Map<String, String> properties = parseAdbDeviceProperties(kAdbShellGetprop);
+      expect(properties, isNotNull);
+      expect(properties['ro.build.characteristics'], 'emulator');
+      expect(properties['ro.product.cpu.abi'], 'x86_64');
+      expect(properties['ro.build.version.sdk'], '23');
+    });
+  });
+
+  group('adb.exe exiting with heap corruption on windows', () {
+    final ProcessManager mockProcessManager = MockProcessManager();
+    String hardware;
+    String buildCharacteristics;
+
+    setUp(() {
+      hardware = 'goldfish';
+      buildCharacteristics = 'unused';
+      exitCode = -1;
+      when(mockProcessManager.run(argThat(contains('getprop')),
+          stderrEncoding: anyNamed('stderrEncoding'),
+          stdoutEncoding: anyNamed('stdoutEncoding'))).thenAnswer((_) {
+        final StringBuffer buf = StringBuffer()
+          ..writeln('[ro.hardware]: [$hardware]')..writeln(
+              '[ro.build.characteristics]: [$buildCharacteristics]');
+        final ProcessResult result = ProcessResult(1, exitCode, buf.toString(), '');
+        return Future<ProcessResult>.value(result);
+      });
+    });
+
+    testUsingContext('nonHeapCorruptionErrorOnWindows', () async {
+      exitCode = -1073740941;
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, false);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        operatingSystem: 'windows',
+        environment: <String, String>{
+          'ANDROID_HOME': '/',
+        },
+      ),
+    });
+
+    testUsingContext('heapCorruptionOnWindows', () async {
+      exitCode = -1073740940;
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, true);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        operatingSystem: 'windows',
+        environment: <String, String>{
+          'ANDROID_HOME': '/',
+        },
+      ),
+    });
+
+    testUsingContext('heapCorruptionExitCodeOnLinux', () async {
+      exitCode = -1073740940;
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, false);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        operatingSystem: 'linux',
+        environment: <String, String>{
+          'ANDROID_HOME': '/',
+        },
+      ),
+    });
+
+    testUsingContext('noErrorOnLinux', () async {
+      exitCode = 0;
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, true);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(
+        operatingSystem: 'linux',
+        environment: <String, String>{
+          'ANDROID_HOME': '/',
+        },
+      ),
+    });
+  });
+
+
+  group('isLocalEmulator', () {
+    final ProcessManager mockProcessManager = MockProcessManager();
+    String hardware;
+    String buildCharacteristics;
+
+    setUp(() {
+      hardware = 'unknown';
+      buildCharacteristics = 'unused';
+      when(mockProcessManager.run(argThat(contains('getprop')),
+          stderrEncoding: anyNamed('stderrEncoding'),
+          stdoutEncoding: anyNamed('stdoutEncoding'))).thenAnswer((_) {
+        final StringBuffer buf = StringBuffer()
+          ..writeln('[ro.hardware]: [$hardware]')
+          ..writeln('[ro.build.characteristics]: [$buildCharacteristics]');
+        final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
+        return Future<ProcessResult>.value(result);
+      });
+    });
+
+    testUsingContext('knownPhysical', () async {
+      hardware = 'samsungexynos7420';
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, false);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('knownEmulator', () async {
+      hardware = 'goldfish';
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, true);
+      expect(await device.supportsHardwareRendering, true);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('unknownPhysical', () async {
+      buildCharacteristics = 'att';
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, false);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('unknownEmulator', () async {
+      buildCharacteristics = 'att,emulator';
+      final AndroidDevice device = AndroidDevice('test');
+      expect(await device.isLocalEmulator, true);
+      expect(await device.supportsHardwareRendering, true);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  testUsingContext('isSupportedForProject is true on module project', () async {
+    fs.file('pubspec.yaml')
+      ..createSync()
+      ..writeAsStringSync(r'''
+name: example
+
+flutter:
+  module: {}
+''');
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(AndroidDevice('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('isSupportedForProject is true with editable host app', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.directory('android').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(AndroidDevice('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('isSupportedForProject is false with no host app and no module', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(AndroidDevice('test').isSupportedForProject(flutterProject), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  group('emulatorId', () {
+    final ProcessManager mockProcessManager = MockProcessManager();
+    const String dummyEmulatorId = 'dummyEmulatorId';
+    final Future<Socket> Function(String host, int port) unresponsiveSocket =
+        (String host, int port) async => MockUnresponsiveAndroidConsoleSocket();
+    final Future<Socket> Function(String host, int port) workingSocket =
+        (String host, int port) async => MockWorkingAndroidConsoleSocket(dummyEmulatorId);
+    String hardware;
+    bool socketWasCreated;
+
+    setUp(() {
+      hardware = 'goldfish'; // Known emulator
+      socketWasCreated = false;
+      when(mockProcessManager.run(argThat(contains('getprop')),
+          stderrEncoding: anyNamed('stderrEncoding'),
+          stdoutEncoding: anyNamed('stdoutEncoding'))).thenAnswer((_) {
+        final StringBuffer buf = StringBuffer()
+          ..writeln('[ro.hardware]: [$hardware]');
+        final ProcessResult result = ProcessResult(1, 0, buf.toString(), '');
+        return Future<ProcessResult>.value(result);
+      });
+    });
+
+    testUsingContext('returns correct ID for responsive emulator', () async {
+      final AndroidDevice device = AndroidDevice('emulator-5555');
+      expect(await device.emulatorId, equals(dummyEmulatorId));
+    }, overrides: <Type, Generator>{
+      AndroidConsoleSocketFactory: () => workingSocket,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('does not create socket for non-emulator devices', () async {
+      hardware = 'samsungexynos7420';
+
+      // Still use an emulator-looking ID so we can be sure the failure is due
+      // to the isLocalEmulator field and not because the ID doesn't contain a
+      // port.
+      final AndroidDevice device = AndroidDevice('emulator-5555');
+      expect(await device.emulatorId, isNull);
+      expect(socketWasCreated, isFalse);
+    }, overrides: <Type, Generator>{
+      AndroidConsoleSocketFactory: () => (String host, int port) async {
+        socketWasCreated = true;
+        throw 'Socket was created for non-emulator';
+      },
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('does not create socket for emulators with no port', () async {
+      final AndroidDevice device = AndroidDevice('emulator-noport');
+      expect(await device.emulatorId, isNull);
+      expect(socketWasCreated, isFalse);
+    }, overrides: <Type, Generator>{
+      AndroidConsoleSocketFactory: () => (String host, int port) async {
+        socketWasCreated = true;
+        throw 'Socket was created for emulator without port in ID';
+      },
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('returns null for connection error', () async {
+      final AndroidDevice device = AndroidDevice('emulator-5555');
+      expect(await device.emulatorId, isNull);
+    }, overrides: <Type, Generator>{
+      AndroidConsoleSocketFactory: () => (String host, int port) => throw 'Fake socket error',
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('returns null for unresponsive device', () async {
+      final AndroidDevice device = AndroidDevice('emulator-5555');
+      expect(await device.emulatorId, isNull);
+    }, overrides: <Type, Generator>{
+      AndroidConsoleSocketFactory: () => unresponsiveSocket,
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('portForwarder', () {
+    final ProcessManager mockProcessManager = MockProcessManager();
+    final AndroidDevice device = AndroidDevice('1234');
+    final DevicePortForwarder forwarder = device.portForwarder;
+
+    testUsingContext('returns the generated host port from stdout', () async {
+      when(mockProcessManager.run(argThat(contains('forward'))))
+      .thenAnswer((_) async => ProcessResult(0, 0, '456', ''));
+
+      expect(await forwarder.forward(123), equals(456));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('returns the supplied host port when stdout is empty', () async {
+      when(mockProcessManager.run(argThat(contains('forward'))))
+      .thenAnswer((_) async => ProcessResult(0, 0, '', ''));
+
+      expect(await forwarder.forward(123, hostPort: 456), equals(456));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('returns the supplied host port when stdout is the host port', () async {
+      when(mockProcessManager.run(argThat(contains('forward'))))
+      .thenAnswer((_) async => ProcessResult(0, 0, '456', ''));
+
+      expect(await forwarder.forward(123, hostPort: 456), equals(456));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('throws an error when stdout is not blank nor the host port', () async {
+      when(mockProcessManager.run(argThat(contains('forward'))))
+      .thenAnswer((_) async => ProcessResult(0, 0, '123456', ''));
+
+      expect(forwarder.forward(123, hostPort: 456), throwsA(isInstanceOf<ProcessException>()));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+const String kAdbShellGetprop = '''
+[dalvik.vm.dex2oat-Xms]: [64m]
+[dalvik.vm.dex2oat-Xmx]: [512m]
+[dalvik.vm.heapsize]: [384m]
+[dalvik.vm.image-dex2oat-Xms]: [64m]
+[dalvik.vm.image-dex2oat-Xmx]: [64m]
+[dalvik.vm.isa.x86.variant]: [dalvik.vm.isa.x86.features=default]
+[dalvik.vm.isa.x86_64.features]: [default]
+[dalvik.vm.isa.x86_64.variant]: [x86_64]
+[dalvik.vm.lockprof.threshold]: [500]
+[dalvik.vm.stack-trace-file]: [/data/anr/traces.txt]
+[debug.atrace.tags.enableflags]: [0]
+[debug.force_rtl]: [0]
+[gsm.current.phone-type]: [1]
+[gsm.network.type]: [Unknown]
+[gsm.nitz.time]: [1473102078793]
+[gsm.operator.alpha]: []
+[gsm.operator.iso-country]: []
+[gsm.operator.isroaming]: [false]
+[gsm.operator.numeric]: []
+[gsm.sim.operator.alpha]: []
+[gsm.sim.operator.iso-country]: []
+[gsm.sim.operator.numeric]: []
+[gsm.sim.state]: [NOT_READY]
+[gsm.version.ril-impl]: [android reference-ril 1.0]
+[init.svc.adbd]: [running]
+[init.svc.bootanim]: [running]
+[init.svc.console]: [running]
+[init.svc.debuggerd]: [running]
+[init.svc.debuggerd64]: [running]
+[init.svc.drm]: [running]
+[init.svc.fingerprintd]: [running]
+[init.svc.gatekeeperd]: [running]
+[init.svc.goldfish-logcat]: [stopped]
+[init.svc.goldfish-setup]: [stopped]
+[init.svc.healthd]: [running]
+[init.svc.installd]: [running]
+[init.svc.keystore]: [running]
+[init.svc.lmkd]: [running]
+[init.svc.logd]: [running]
+[init.svc.logd-reinit]: [stopped]
+[init.svc.media]: [running]
+[init.svc.netd]: [running]
+[init.svc.perfprofd]: [running]
+[init.svc.qemu-props]: [stopped]
+[init.svc.ril-daemon]: [running]
+[init.svc.servicemanager]: [running]
+[init.svc.surfaceflinger]: [running]
+[init.svc.ueventd]: [running]
+[init.svc.vold]: [running]
+[init.svc.zygote]: [running]
+[init.svc.zygote_secondary]: [running]
+[net.bt.name]: [Android]
+[net.change]: [net.qtaguid_enabled]
+[net.eth0.dns1]: [10.0.2.3]
+[net.eth0.gw]: [10.0.2.2]
+[net.gprs.local-ip]: [10.0.2.15]
+[net.hostname]: [android-ccd858aa3d3825ee]
+[net.qtaguid_enabled]: [1]
+[net.tcp.default_init_rwnd]: [60]
+[persist.sys.dalvik.vm.lib.2]: [libart.so]
+[persist.sys.profiler_ms]: [0]
+[persist.sys.timezone]: [America/Los_Angeles]
+[persist.sys.usb.config]: [adb]
+[qemu.gles]: [1]
+[qemu.hw.mainkeys]: [0]
+[qemu.sf.fake_camera]: [none]
+[qemu.sf.lcd_density]: [420]
+[rild.libargs]: [-d /dev/ttyS0]
+[rild.libpath]: [/system/lib/libreference-ril.so]
+[ro.allow.mock.location]: [0]
+[ro.baseband]: [unknown]
+[ro.board.platform]: []
+[ro.boot.hardware]: [ranchu]
+[ro.bootimage.build.date]: [Wed Jul 20 21:03:09 UTC 2016]
+[ro.bootimage.build.date.utc]: [1469048589]
+[ro.bootimage.build.fingerprint]: [Android/sdk_google_phone_x86_64/generic_x86_64:6.0/MASTER/3079352:userdebug/test-keys]
+[ro.bootloader]: [unknown]
+[ro.bootmode]: [unknown]
+[ro.build.characteristics]: [emulator]
+[ro.build.date]: [Wed Jul 20 21:02:14 UTC 2016]
+[ro.build.date.utc]: [1469048534]
+[ro.build.description]: [sdk_google_phone_x86_64-userdebug 6.0 MASTER 3079352 test-keys]
+[ro.build.display.id]: [sdk_google_phone_x86_64-userdebug 6.0 MASTER 3079352 test-keys]
+[ro.build.fingerprint]: [Android/sdk_google_phone_x86_64/generic_x86_64:6.0/MASTER/3079352:userdebug/test-keys]
+[ro.build.flavor]: [sdk_google_phone_x86_64-userdebug]
+[ro.build.host]: [vpba14.mtv.corp.google.com]
+[ro.build.id]: [MASTER]
+[ro.build.product]: [generic_x86_64]
+[ro.build.tags]: [test-keys]
+[ro.build.type]: [userdebug]
+[ro.build.user]: [android-build]
+[ro.build.version.all_codenames]: [REL]
+[ro.build.version.base_os]: []
+[ro.build.version.codename]: [REL]
+[ro.build.version.incremental]: [3079352]
+[ro.build.version.preview_sdk]: [0]
+[ro.build.version.release]: [6.0]
+[ro.build.version.sdk]: [23]
+[ro.build.version.security_patch]: [2015-10-01]
+[ro.com.google.locationfeatures]: [1]
+[ro.config.alarm_alert]: [Alarm_Classic.ogg]
+[ro.config.nocheckin]: [yes]
+[ro.config.notification_sound]: [OnTheHunt.ogg]
+[ro.crypto.state]: [unencrypted]
+[ro.dalvik.vm.native.bridge]: [0]
+[ro.debuggable]: [1]
+[ro.hardware]: [ranchu]
+[ro.hardware.audio.primary]: [goldfish]
+[ro.hwui.drop_shadow_cache_size]: [6]
+[ro.hwui.gradient_cache_size]: [1]
+[ro.hwui.layer_cache_size]: [48]
+[ro.hwui.path_cache_size]: [32]
+[ro.hwui.r_buffer_cache_size]: [8]
+[ro.hwui.text_large_cache_height]: [1024]
+[ro.hwui.text_large_cache_width]: [2048]
+[ro.hwui.text_small_cache_height]: [1024]
+[ro.hwui.text_small_cache_width]: [1024]
+[ro.hwui.texture_cache_flushrate]: [0.4]
+[ro.hwui.texture_cache_size]: [72]
+[ro.kernel.android.checkjni]: [1]
+[ro.kernel.android.qemud]: [1]
+[ro.kernel.androidboot.hardware]: [ranchu]
+[ro.kernel.clocksource]: [pit]
+[ro.kernel.qemu]: [1]
+[ro.kernel.qemu.gles]: [1]
+[ro.opengles.version]: [131072]
+[ro.product.board]: []
+[ro.product.brand]: [Android]
+[ro.product.cpu.abi]: [x86_64]
+[ro.product.cpu.abilist]: [x86_64,x86]
+[ro.product.cpu.abilist32]: [x86]
+[ro.product.cpu.abilist64]: [x86_64]
+[ro.product.device]: [generic_x86_64]
+[ro.product.locale]: [en-US]
+[ro.product.manufacturer]: [unknown]
+[ro.product.model]: [Android SDK built for x86_64]
+[ro.product.name]: [sdk_google_phone_x86_64]
+[ro.radio.use-ppp]: [no]
+[ro.revision]: [0]
+[ro.secure]: [1]
+[ro.serialno]: []
+[ro.wifi.channels]: []
+[ro.zygote]: [zygote64_32]
+[selinux.reload_policy]: [1]
+[service.bootanim.exit]: [0]
+[status.battery.level]: [5]
+[status.battery.level_raw]: [50]
+[status.battery.level_scale]: [9]
+[status.battery.state]: [Slow]
+[sys.sysctl.extra_free_kbytes]: [24300]
+[sys.usb.config]: [adb]
+[sys.usb.state]: [adb]
+[vold.has_adoptable]: [1]
+[wlan.driver.status]: [unloaded]
+[xmpp.auto-presence]: [true]
+''';
+
+/// A mock Android Console that presents a connection banner and responds to
+/// "avd name" requests with the supplied name.
+class MockWorkingAndroidConsoleSocket extends Mock implements Socket {
+  MockWorkingAndroidConsoleSocket(this.avdName) {
+    _controller.add('Android Console: Welcome!\n');
+    // Include OK in the same packet here. In the response to "avd name"
+    // it's sent alone to ensure both are handled.
+    _controller.add('Android Console: Some intro text\nOK\n');
+  }
+
+  final String avdName;
+  final StreamController<String> _controller = StreamController<String>();
+
+  @override
+  Stream<E> asyncMap<E>(FutureOr<E> convert(Uint8List event)) => _controller.stream as Stream<E>;
+
+  @override
+  void add(List<int> data) {
+    final String text = ascii.decode(data);
+    if (text == 'avd name\n') {
+      _controller.add('$avdName\n');
+      // Include OK in its own packet here. In welcome banner it's included
+      // as part of the previous text to ensure both are handled.
+      _controller.add('OK\n');
+    } else {
+      throw 'Unexpected command $text';
+    }
+  }
+}
+
+/// An Android console socket that drops all input and returns no output.
+class MockUnresponsiveAndroidConsoleSocket extends Mock implements Socket {
+  final StreamController<String> _controller = StreamController<String>();
+
+  @override
+  Stream<E> asyncMap<E>(FutureOr<E> convert(Uint8List event)) => _controller.stream as Stream<E>;
+
+  @override
+  void add(List<int> data) {}
+}
diff --git a/packages/flutter_tools/test/general.shard/android/android_emulator_test.dart b/packages/flutter_tools/test/general.shard/android/android_emulator_test.dart
new file mode 100644
index 0000000..cee950b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_emulator_test.dart
@@ -0,0 +1,77 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/android/android_emulator.dart';
+import 'package:flutter_tools/src/device.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('android_emulator', () {
+    testUsingContext('flags emulators without config', () {
+      const String emulatorID = '1234';
+      final AndroidEmulator emulator = AndroidEmulator(emulatorID);
+      expect(emulator.id, emulatorID);
+      expect(emulator.hasConfig, false);
+    });
+    testUsingContext('flags emulators with config', () {
+      const String emulatorID = '1234';
+      final AndroidEmulator emulator =
+          AndroidEmulator(emulatorID, <String, String>{'name': 'test'});
+      expect(emulator.id, emulatorID);
+      expect(emulator.hasConfig, true);
+    });
+    testUsingContext('reads expected metadata', () {
+      const String emulatorID = '1234';
+      const String manufacturer = 'Me';
+      const String displayName = 'The best one';
+      final Map<String, String> properties = <String, String>{
+        'hw.device.manufacturer': manufacturer,
+        'avd.ini.displayname': displayName,
+      };
+      final AndroidEmulator emulator =
+          AndroidEmulator(emulatorID, properties);
+      expect(emulator.id, emulatorID);
+      expect(emulator.name, displayName);
+      expect(emulator.manufacturer, manufacturer);
+      expect(emulator.category, Category.mobile);
+      expect(emulator.platformType, PlatformType.android);
+    });
+    testUsingContext('prefers displayname for name', () {
+      const String emulatorID = '1234';
+      const String displayName = 'The best one';
+      final Map<String, String> properties = <String, String>{
+        'avd.ini.displayname': displayName,
+      };
+      final AndroidEmulator emulator =
+          AndroidEmulator(emulatorID, properties);
+      expect(emulator.name, displayName);
+    });
+    testUsingContext('uses cleaned up ID if no displayname is set', () {
+      // Android Studio uses the ID with underscores replaced with spaces
+      // for the name if displayname is not set so we do the same.
+      const String emulatorID = 'This_is_my_ID';
+      final Map<String, String> properties = <String, String>{
+        'avd.ini.notadisplayname': 'this is not a display name',
+      };
+      final AndroidEmulator emulator =
+          AndroidEmulator(emulatorID, properties);
+      expect(emulator.name, 'This is my ID');
+    });
+    testUsingContext('parses ini files', () {
+      const String iniFile = '''
+        hw.device.name=My Test Name
+        #hw.device.name=Bad Name
+
+        hw.device.manufacturer=Me
+        avd.ini.displayname = dispName
+      ''';
+      final Map<String, String> results = parseIniLines(iniFile.split('\n'));
+      expect(results['hw.device.name'], 'My Test Name');
+      expect(results['hw.device.manufacturer'], 'Me');
+      expect(results['avd.ini.displayname'], 'dispName');
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart
new file mode 100644
index 0000000..e08151b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart
@@ -0,0 +1,249 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart' show ProcessResult;
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+void main() {
+  MemoryFileSystem fs;
+  MockProcessManager processManager;
+
+  setUp(() {
+    fs = MemoryFileSystem();
+    processManager = MockProcessManager();
+  });
+
+  group('android_sdk AndroidSdk', () {
+    Directory sdkDir;
+
+    tearDown(() {
+      if (sdkDir != null) {
+        tryToDelete(sdkDir);
+        sdkDir = null;
+      }
+    });
+
+    testUsingContext('parse sdk', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      expect(sdk.latestVersion, isNotNull);
+      expect(sdk.latestVersion.sdkLevel, 23);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('parse sdk N', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory(withAndroidN: true);
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      expect(sdk.latestVersion, isNotNull);
+      expect(sdk.latestVersion.sdkLevel, 24);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns sdkmanager path', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      expect(sdk.sdkManagerPath, fs.path.join(sdk.directory, 'tools', 'bin', 'sdkmanager'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns sdkmanager version', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      when(processManager.canRun(sdk.sdkManagerPath)).thenReturn(true);
+      when(processManager.runSync(<String>[sdk.sdkManagerPath, '--version'],
+          environment: argThat(isNotNull,  named: 'environment')))
+          .thenReturn(ProcessResult(1, 0, '26.1.1\n', ''));
+      expect(sdk.sdkManagerVersion, '26.1.1');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('returns validate sdk is well formed', () {
+      sdkDir = MockBrokenAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      when(processManager.canRun(sdk.adbPath)).thenReturn(true);
+
+      final List<String> validationIssues = sdk.validateSdkWellFormed();
+      expect(validationIssues.first, 'No valid Android SDK platforms found in'
+        ' /.tmp_rand0/flutter_mock_android_sdk.rand0/platforms. Candidates were:\n'
+        '  - android-22\n'
+        '  - android-23');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('does not throw on sdkmanager version check failure', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory();
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      when(processManager.canRun(sdk.sdkManagerPath)).thenReturn(true);
+      when(processManager.runSync(<String>[sdk.sdkManagerPath, '--version'],
+          environment: argThat(isNotNull,  named: 'environment')))
+          .thenReturn(ProcessResult(1, 1, '26.1.1\n', 'Mystery error'));
+      expect(sdk.sdkManagerVersion, isNull);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('throws on sdkmanager version check if sdkmanager not found', () {
+      sdkDir = MockAndroidSdk.createSdkDirectory(withSdkManager: false);
+      Config.instance.setValue('android-sdk', sdkDir.path);
+
+      final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+      when(processManager.canRun(sdk.sdkManagerPath)).thenReturn(false);
+      expect(() => sdk.sdkManagerVersion, throwsToolExit());
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => processManager,
+    });
+
+    group('ndk', () {
+      const <String, String>{
+        'linux': 'linux-x86_64',
+        'macos': 'darwin-x86_64',
+      }.forEach((String os, String osDir) {
+        testUsingContext('detection on $os', () {
+          sdkDir = MockAndroidSdk.createSdkDirectory(
+              withAndroidN: true, withNdkDir: osDir, withNdkSysroot: true);
+          Config.instance.setValue('android-sdk', sdkDir.path);
+
+          final String realSdkDir = sdkDir.path;
+          final String realNdkDir = fs.path.join(realSdkDir, 'ndk-bundle');
+          final String realNdkCompiler = fs.path.join(
+              realNdkDir,
+              'toolchains',
+              'arm-linux-androideabi-4.9',
+              'prebuilt',
+              osDir,
+              'bin',
+              'arm-linux-androideabi-gcc');
+          final String realNdkSysroot =
+              fs.path.join(realNdkDir, 'platforms', 'android-9', 'arch-arm');
+
+          final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+          expect(sdk.directory, realSdkDir);
+          expect(sdk.ndk, isNotNull);
+          expect(sdk.ndk.directory, realNdkDir);
+          expect(sdk.ndk.compiler, realNdkCompiler);
+          expect(sdk.ndk.compilerArgs, <String>['--sysroot', realNdkSysroot]);
+        }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          Platform: () => FakePlatform(operatingSystem: os),
+        });
+
+        testUsingContext('newer NDK require explicit -fuse-ld on $os', () {
+          sdkDir = MockAndroidSdk.createSdkDirectory(
+              withAndroidN: true, withNdkDir: osDir, withNdkSysroot: true, ndkVersion: 18);
+          Config.instance.setValue('android-sdk', sdkDir.path);
+
+          final String realSdkDir = sdkDir.path;
+          final String realNdkDir = fs.path.join(realSdkDir, 'ndk-bundle');
+          final String realNdkToolchainBin = fs.path.join(
+              realNdkDir,
+              'toolchains',
+              'arm-linux-androideabi-4.9',
+              'prebuilt',
+              osDir,
+              'bin');
+          final String realNdkCompiler = fs.path.join(
+              realNdkToolchainBin,
+              'arm-linux-androideabi-gcc');
+          final String realNdkLinker = fs.path.join(
+              realNdkToolchainBin,
+              'arm-linux-androideabi-ld');
+          final String realNdkSysroot =
+              fs.path.join(realNdkDir, 'platforms', 'android-9', 'arch-arm');
+
+          final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+          expect(sdk.directory, realSdkDir);
+          expect(sdk.ndk, isNotNull);
+          expect(sdk.ndk.directory, realNdkDir);
+          expect(sdk.ndk.compiler, realNdkCompiler);
+          expect(sdk.ndk.compilerArgs, <String>['--sysroot', realNdkSysroot, '-fuse-ld=$realNdkLinker']);
+        }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          Platform: () => FakePlatform(operatingSystem: os),
+        });
+      });
+
+      for (String os in <String>['linux', 'macos']) {
+        testUsingContext('detection on $os (no ndk available)', () {
+          sdkDir = MockAndroidSdk.createSdkDirectory(withAndroidN: true);
+          Config.instance.setValue('android-sdk', sdkDir.path);
+
+          final String realSdkDir = sdkDir.path;
+          final AndroidSdk sdk = AndroidSdk.locateAndroidSdk();
+          expect(sdk.directory, realSdkDir);
+          expect(sdk.ndk, isNull);
+          final String explanation = AndroidNdk.explainMissingNdk(sdk.directory);
+          expect(explanation, contains('Can not locate ndk-bundle'));
+        }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          Platform: () => FakePlatform(operatingSystem: os),
+        });
+      }
+    });
+  });
+}
+
+/// A broken SDK installation.
+class MockBrokenAndroidSdk extends Mock implements AndroidSdk {
+  static Directory createSdkDirectory({
+    bool withAndroidN = false,
+    String withNdkDir,
+    bool withNdkSysroot = false,
+    bool withSdkManager = true,
+  }) {
+    final Directory dir = fs.systemTempDirectory.createTempSync('flutter_mock_android_sdk.');
+    final String exe = platform.isWindows ? '.exe' : '';
+    _createSdkFile(dir, 'licenses/dummy');
+    _createSdkFile(dir, 'platform-tools/adb$exe');
+
+    _createSdkFile(dir, 'build-tools/sda/aapt$exe');
+    _createSdkFile(dir, 'build-tools/af/aapt$exe');
+    _createSdkFile(dir, 'build-tools/ljkasd/aapt$exe');
+
+    _createSdkFile(dir, 'platforms/android-22/android.jar');
+    _createSdkFile(dir, 'platforms/android-23/android.jar');
+
+    return dir;
+  }
+
+  static void _createSdkFile(Directory dir, String filePath, { String contents }) {
+    final File file = dir.childFile(filePath);
+    file.createSync(recursive: true);
+    if (contents != null) {
+      file.writeAsStringSync(contents, flush: true);
+    }
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/android/android_studio_test.dart b/packages/flutter_tools/test/general.shard/android/android_studio_test.dart
new file mode 100644
index 0000000..69a7a8d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_studio_test.dart
@@ -0,0 +1,175 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/android_studio.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+const String homeLinux = '/home/me';
+const String homeMac = '/Users/me';
+
+const String macStudioInfoPlistValue =
+'''
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>CFBundleGetInfoString</key>
+    <string>Android Studio 3.3, build AI-182.5107.16.33.5199772. Copyright JetBrains s.r.o., (c) 2000-2018</string>
+    <key>CFBundleShortVersionString</key>
+    <string>3.3</string>
+    <key>CFBundleVersion</key>
+    <string>AI-182.5107.16.33.5199772</string>
+    <key>JVMOptions</key>
+    <dict>
+      <key>Properties</key>
+      <dict>
+        <key>idea.platform.prefix</key>
+        <string>AndroidStudio</string>
+        <key>idea.paths.selector</key>
+        <string>AndroidStudio3.3</string>
+      </dict>
+    </dict>
+  </dict>
+</plist>
+      ''';
+const String macStudioInfoPlistDefaultsResult =
+'''
+{
+    CFBundleGetInfoString = "Android Studio 3.3, build AI-182.5107.16.33.5199772. Copyright JetBrains s.r.o., (c) 2000-2018";
+    CFBundleShortVersionString = "3.3";
+    CFBundleVersion = "AI-182.5107.16.33.5199772";
+    JVMOptions =     {
+        Properties =         {
+            "idea.paths.selector" = "AndroidStudio3.3";
+            "idea.platform.prefix" = AndroidStudio;
+        };
+    };
+}
+''';
+
+class MockIOSWorkflow extends Mock implements IOSWorkflow {}
+
+Platform linuxPlatform() {
+  return FakePlatform.fromPlatform(const LocalPlatform())
+    ..operatingSystem = 'linux'
+    ..environment = <String, String>{'HOME': homeLinux};
+}
+
+Platform macPlatform() {
+  return FakePlatform.fromPlatform(const LocalPlatform())
+    ..operatingSystem = 'macos'
+    ..environment = <String, String>{'HOME': homeMac};
+}
+
+void main() {
+  MemoryFileSystem fs;
+  MockIOSWorkflow iosWorkflow;
+
+  setUp(() {
+    fs = MemoryFileSystem();
+    iosWorkflow = MockIOSWorkflow();
+  });
+
+  group('pluginsPath on Linux', () {
+    testUsingContext('extracts custom paths from home dir', () {
+      const String installPath = '/opt/android-studio-with-cheese-5.0';
+      const String studioHome = '$homeLinux/.AndroidStudioWithCheese5.0';
+      const String homeFile = '$studioHome/system/.home';
+      fs.directory(installPath).createSync(recursive: true);
+      fs.file(homeFile).createSync(recursive: true);
+      fs.file(homeFile).writeAsStringSync(installPath);
+
+      final AndroidStudio studio =
+      AndroidStudio.fromHomeDot(fs.directory(studioHome));
+      expect(studio, isNotNull);
+      expect(studio.pluginsPath,
+          equals('/home/me/.AndroidStudioWithCheese5.0/config/plugins'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      // Custom home paths are not supported on macOS nor Windows yet,
+      // so we force the platform to fake Linux here.
+      Platform: () => linuxPlatform(),
+    });
+  });
+
+  group('pluginsPath on Mac', () {
+    testUsingContext('extracts custom paths for directly downloaded Android Studio on Mac', () {
+      final String studioInApplicationPlistFolder = fs.path.join('/', 'Application', 'Android Studio.app', 'Contents');
+      fs.directory(studioInApplicationPlistFolder).createSync(recursive: true);
+
+      final String plistFilePath = fs.path.join(studioInApplicationPlistFolder, 'Info.plist');
+      fs.file(plistFilePath).writeAsStringSync(macStudioInfoPlistValue);
+      when(iosWorkflow.getPlistValueFromFile(plistFilePath, null)).thenReturn(macStudioInfoPlistDefaultsResult);
+      final AndroidStudio studio = AndroidStudio.fromMacOSBundle(fs.directory(studioInApplicationPlistFolder)?.parent?.path);
+      expect(studio, isNotNull);
+      expect(studio.pluginsPath,
+          equals(fs.path.join(homeMac, 'Library', 'Application Support', 'AndroidStudio3.3')));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      // Custom home paths are not supported on macOS nor Windows yet,
+      // so we force the platform to fake Linux here.
+      Platform: () => macPlatform(),
+      IOSWorkflow: () => iosWorkflow,
+    });
+
+    testUsingContext('extracts custom paths for Android Studio downloaded by JetBrainsToolbox on Mac', () {
+      final String jetbrainsStudioInApplicationPlistFolder = fs.path.join(homeMac, 'Application', 'JetBrains Toolbox', 'Android Studio.app', 'Contents');
+      fs.directory(jetbrainsStudioInApplicationPlistFolder).createSync(recursive: true);
+      const String jetbrainsInfoPlistValue =
+      '''
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC '-//Apple Computer//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>
+<plist version="1.0">
+ <dict>
+  <key>CFBundleVersion</key>
+  <string>3.3</string>
+  <key>CFBundleLongVersionString</key>
+  <string>3.3</string>
+  <key>CFBundleShortVersionString</key>
+  <string>3.3</string>
+  <key>JetBrainsToolboxApp</key>
+  <string>$homeMac/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/183.5256920/Android Studio 3.3</string>
+ </dict>
+</plist>
+      ''';
+      const String jetbrainsInfoPlistDefaultsResult =
+      '''
+{
+    CFBundleLongVersionString = "3.3";
+    CFBundleShortVersionString = "3.3";
+    CFBundleVersion = "3.3";
+    JetBrainsToolboxApp = "$homeMac/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/183.5256920/Android Studio 3.3.app";
+}
+''';
+      final String jetbrainsPlistFilePath = fs.path.join(jetbrainsStudioInApplicationPlistFolder, 'Info.plist');
+      fs.file(jetbrainsPlistFilePath).writeAsStringSync(jetbrainsInfoPlistValue);
+      when(iosWorkflow.getPlistValueFromFile(jetbrainsPlistFilePath, null)).thenReturn(jetbrainsInfoPlistDefaultsResult);
+
+      final String studioInApplicationPlistFolder = fs.path.join(fs.path.join(homeMac, 'Library', 'Application Support'), 'JetBrains', 'Toolbox', 'apps', 'AndroidStudio', 'ch-0', '183.5256920', fs.path.join('Android Studio 3.3.app', 'Contents'));
+      fs.directory(studioInApplicationPlistFolder).createSync(recursive: true);
+      final String studioPlistFilePath = fs.path.join(studioInApplicationPlistFolder, 'Info.plist');
+      fs.file(studioPlistFilePath).writeAsStringSync(macStudioInfoPlistValue);
+      when(iosWorkflow.getPlistValueFromFile(studioPlistFilePath, null)).thenReturn(macStudioInfoPlistDefaultsResult);
+
+      final AndroidStudio studio = AndroidStudio.fromMacOSBundle(fs.directory(jetbrainsStudioInApplicationPlistFolder)?.parent?.path);
+      expect(studio, isNotNull);
+      expect(studio.pluginsPath,
+          equals(fs.path.join(homeMac, 'Library', 'Application Support', 'AndroidStudio3.3')));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      // Custom home paths are not supported on macOS nor Windows yet,
+      // so we force the platform to fake Linux here.
+      Platform: () => macPlatform(),
+      IOSWorkflow: () => iosWorkflow,
+    });
+
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/android/android_studio_validator_test.dart b/packages/flutter_tools/test/general.shard/android/android_studio_validator_test.dart
new file mode 100644
index 0000000..49d5691
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_studio_validator_test.dart
@@ -0,0 +1,31 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/android/android_studio_validator.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+const String home = '/home/me';
+
+Platform linuxPlatform() {
+  return FakePlatform.fromPlatform(const LocalPlatform())
+    ..operatingSystem = 'linux'
+    ..environment = <String, String>{'HOME': home};
+}
+
+void main() {
+  group('NoAndroidStudioValidator', () {
+    testUsingContext('shows Android Studio as "not available" when not available.', () async {
+      final NoAndroidStudioValidator validator = NoAndroidStudioValidator();
+      expect((await validator.validate()).type, equals(ValidationType.notAvailable));
+    }, overrides: <Type, Generator>{
+      // Custom home paths are not supported on macOS nor Windows yet,
+      // so we force the platform to fake Linux here.
+      Platform: () => linuxPlatform(),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart
new file mode 100644
index 0000000..3ebd229
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart
@@ -0,0 +1,253 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/android/android_workflow.dart';
+import 'package:flutter_tools/src/base/user_messages.dart';
+import 'package:flutter_tools/src/base/version.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart' show MockAndroidSdk, MockProcess, MockProcessManager, MockStdio;
+
+class MockAndroidSdkVersion extends Mock implements AndroidSdkVersion {}
+
+void main() {
+  AndroidSdk sdk;
+  MemoryFileSystem fs;
+  MockProcessManager processManager;
+  MockStdio stdio;
+
+  setUp(() {
+    sdk = MockAndroidSdk();
+    fs = MemoryFileSystem();
+    fs.directory('/home/me').createSync(recursive: true);
+    processManager = MockProcessManager();
+    stdio = MockStdio();
+  });
+
+  MockProcess Function(List<String>) processMetaFactory(List<String> stdout) {
+    final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(
+        stdout.map<List<int>>((String s) => s.codeUnits));
+    return (List<String> command) => MockProcess(stdout: stdoutStream);
+  }
+
+  testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
+    processManager.succeed = false;
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
+    expect(licenseStatus, LicensesAccepted.unknown);
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('licensesAccepted handles garbage/no output', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted result = await licenseValidator.licensesAccepted;
+    expect(result, equals(LicensesAccepted.unknown));
+    expect(processManager.commands.first, equals('/foo/bar/sdkmanager'));
+    expect(processManager.commands.last, equals('--licenses'));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('licensesAccepted works for all licenses accepted', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    processManager.processFactory = processMetaFactory(<String>[
+       '[=======================================] 100% Computing updates...             ',
+       'All SDK package licenses accepted.',
+    ]);
+
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted result = await licenseValidator.licensesAccepted;
+    expect(result, equals(LicensesAccepted.all));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('licensesAccepted works for some licenses accepted', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    processManager.processFactory = processMetaFactory(<String>[
+      '[=======================================] 100% Computing updates...             ',
+      '2 of 5 SDK package licenses not accepted.',
+      'Review licenses that have not been accepted (y/N)?',
+    ]);
+
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted result = await licenseValidator.licensesAccepted;
+    expect(result, equals(LicensesAccepted.some));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('licensesAccepted works for no licenses accepted', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    processManager.processFactory = processMetaFactory(<String>[
+      '[=======================================] 100% Computing updates...             ',
+      '5 of 5 SDK package licenses not accepted.',
+      'Review licenses that have not been accepted (y/N)?',
+    ]);
+
+    final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
+    final LicensesAccepted result = await licenseValidator.licensesAccepted;
+    expect(result, equals(LicensesAccepted.none));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('runLicenseManager succeeds for version >= 26', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    when(sdk.sdkManagerVersion).thenReturn('26.0.0');
+
+    expect(await AndroidLicenseValidator.runLicenseManager(), isTrue);
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('runLicenseManager errors for version < 26', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    when(sdk.sdkManagerVersion).thenReturn('25.0.0');
+
+    expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit(message: 'To update, run'));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('runLicenseManager errors correctly for null version', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    when(sdk.sdkManagerVersion).thenReturn(null);
+
+    expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit(message: 'To update, run'));
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('runLicenseManager errors when sdkmanager is not found', () async {
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    processManager.succeed = false;
+
+    expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit());
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('detects license-only SDK installation', () async {
+    when(sdk.licensesAvailable).thenReturn(true);
+    when(sdk.platformToolsAvailable).thenReturn(false);
+    final ValidationResult validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.partial);
+    expect(
+      validationResult.messages.last.message,
+      userMessages.androidSdkLicenseOnly(kAndroidHome),
+    );
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+  testUsingContext('detects minium required SDK and buildtools', () async {
+    final AndroidSdkVersion mockSdkVersion = MockAndroidSdkVersion();
+    when(sdk.licensesAvailable).thenReturn(true);
+    when(sdk.platformToolsAvailable).thenReturn(true);
+
+    // Test with invalid SDK and build tools
+    when(mockSdkVersion.sdkLevel).thenReturn(26);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(Version(26, 0, 3));
+    when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
+    when(sdk.latestVersion).thenReturn(mockSdkVersion);
+    when(sdk.validateSdkWellFormed()).thenReturn(<String>[]);
+    final String errorMessage = userMessages.androidSdkBuildToolsOutdated(
+      sdk.sdkManagerPath,
+      kAndroidSdkMinVersion,
+      kAndroidSdkBuildToolsMinVersion.toString(),
+    );
+
+    ValidationResult validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.missing);
+    expect(
+      validationResult.messages.last.message,
+      errorMessage,
+    );
+
+    // Test with valid SDK but invalid build tools
+    when(mockSdkVersion.sdkLevel).thenReturn(28);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(Version(28, 0, 2));
+
+    validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.missing);
+    expect(
+      validationResult.messages.last.message,
+      errorMessage,
+    );
+
+    // Test with valid SDK and valid build tools
+    // Will still be partial because AnroidSdk.findJavaBinary is static :(
+    when(mockSdkVersion.sdkLevel).thenReturn(kAndroidSdkMinVersion);
+    when(mockSdkVersion.buildToolsVersion).thenReturn(kAndroidSdkBuildToolsMinVersion);
+
+    validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.partial); // No Java binary
+    expect(
+      validationResult.messages.any((ValidationMessage message) => message.message == errorMessage),
+      isFalse,
+    );
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
+}
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
new file mode 100644
index 0000000..b09f73f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -0,0 +1,552 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/android/gradle.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/pubspec_schema.dart';
+
+void main() {
+  Cache.flutterRoot = getFlutterRoot();
+  group('gradle build', () {
+    test('do not crash if there is no Android SDK', () async {
+      Exception shouldBeToolExit;
+      try {
+        // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
+        // currently impossible as the test is not hermetic. Luckily, our bots don't have Android
+        // SDKs yet so androidSdk should be null by default.
+        //
+        // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
+        // will be null and our expectation would fail. That would remind us to make these tests
+        // hermetic before adding Android SDKs to the bots.
+        updateLocalProperties(project: FlutterProject.current());
+      } on Exception catch (e) {
+        shouldBeToolExit = e;
+      }
+      // Ensure that we throw a meaningful ToolExit instead of a general crash.
+      expect(shouldBeToolExit, isToolExit);
+    });
+
+    // Regression test for https://github.com/flutter/flutter/issues/34700
+    testUsingContext('Does not return nulls in apk list', () {
+      final GradleProject gradleProject = MockGradleProject();
+      const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
+      when(gradleProject.apkFilesFor(buildInfo)).thenReturn(<String>['not_real']);
+      when(gradleProject.apkDirectory).thenReturn(fs.currentDirectory);
+
+      expect(findApkFiles(gradleProject, buildInfo), <File>[]);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
+
+    test('androidXFailureRegex should match lines with likely AndroidX errors', () {
+      final List<String> nonMatchingLines = <String>[
+        ':app:preBuild UP-TO-DATE',
+        'BUILD SUCCESSFUL in 0s',
+        '',
+      ];
+      final List<String> matchingLines = <String>[
+        'AAPT: error: resource android:attr/fontVariationSettings not found.',
+        'AAPT: error: resource android:attr/ttcIndex not found.',
+        'error: package android.support.annotation does not exist',
+        'import android.support.annotation.NonNull;',
+        'import androidx.annotation.NonNull;',
+        'Daemon:  AAPT2 aapt2-3.2.1-4818971-linux Daemon #0',
+      ];
+      for (String m in nonMatchingLines) {
+        expect(androidXFailureRegex.hasMatch(m), isFalse);
+      }
+      for (String m in matchingLines) {
+        expect(androidXFailureRegex.hasMatch(m), isTrue);
+      }
+    });
+
+    test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
+      final List<String> nonMatchingLines = <String>[
+        ':app:preBuild UP-TO-DATE',
+        'BUILD SUCCESSFUL in 0s',
+        'Generic plugin AndroidX text',
+        '',
+      ];
+      final List<String> matchingLines = <String>[
+        '*********************************************************************************************************************************',
+        "WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
+        'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
+        'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
+      ];
+      for (String m in nonMatchingLines) {
+        expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
+      }
+      for (String m in matchingLines) {
+        expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
+      }
+    });
+
+    test('ndkMessageFilter should only match lines without the error message', () {
+      final List<String> nonMatchingLines = <String>[
+        'NDK is missing a "platforms" directory.',
+        'If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to /usr/local/company/home/username/Android/Sdk/ndk-bundle.',
+        'If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.',
+      ];
+      final List<String> matchingLines = <String>[
+        ':app:preBuild UP-TO-DATE',
+        'BUILD SUCCESSFUL in 0s',
+        '',
+        'Something NDK related mentioning ANDROID_NDK_HOME',
+      ];
+      for (String m in nonMatchingLines) {
+        expect(ndkMessageFilter.hasMatch(m), isFalse);
+      }
+      for (String m in matchingLines) {
+        expect(ndkMessageFilter.hasMatch(m), isTrue);
+      }
+    });
+  });
+
+  group('gradle project', () {
+    GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);
+
+    test('should extract build directory from app properties', () {
+      final GradleProject project = projectFrom('''
+someProperty: someValue
+buildDir: /Users/some/apps/hello/build/app
+someOtherProperty: someOtherValue
+      ''', '');
+      expect(
+        fs.path.normalize(project.apkDirectory.path),
+        fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'),
+      );
+    });
+    test('should extract default build variants from app properties', () {
+      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
+someTask
+assemble
+assembleAndroidTest
+assembleDebug
+assembleProfile
+assembleRelease
+someOtherTask
+      ''');
+      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
+      expect(project.productFlavors, isEmpty);
+    });
+    test('should extract custom build variants from app properties', () {
+      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
+someTask
+assemble
+assembleAndroidTest
+assembleDebug
+assembleFree
+assembleFreeAndroidTest
+assembleFreeDebug
+assembleFreeProfile
+assembleFreeRelease
+assemblePaid
+assemblePaidAndroidTest
+assemblePaidDebug
+assemblePaidProfile
+assemblePaidRelease
+assembleProfile
+assembleRelease
+someOtherTask
+      ''');
+      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
+      expect(project.productFlavors, <String>['free', 'paid']);
+    });
+    test('should provide apk file name for default build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.debug)).first, 'app-debug.apk');
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.profile)).first, 'app-profile.apk');
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.release)).first, 'app-release.apk');
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
+    });
+    test('should provide apk file name for flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'free'))).first, 'app-free-debug.apk');
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'paid'))).first, 'app-paid-release.apk');
+      expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
+    });
+    test('should provide apks for default build types and each ABI', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo.debug,
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ),
+        <String>[
+          'app-armeabi-v7a-debug.apk',
+          'app-arm64-v8a-debug.apk',
+        ]);
+
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo.release,
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ),
+        <String>[
+          'app-armeabi-v7a-release.apk',
+          'app-arm64-v8a-release.apk',
+        ]);
+
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo(BuildMode.release, 'unknown'),
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ).isEmpty, isTrue);
+    });
+    test('should provide apks for each ABI and flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo(BuildMode.debug, 'free'),
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ),
+        <String>[
+          'app-free-armeabi-v7a-debug.apk',
+          'app-free-arm64-v8a-debug.apk',
+        ]);
+
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo(BuildMode.release, 'paid'),
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ),
+        <String>[
+          'app-paid-armeabi-v7a-release.apk',
+          'app-paid-arm64-v8a-release.apk',
+        ]);
+
+      expect(project.apkFilesFor(
+        const AndroidBuildInfo(
+          BuildInfo(BuildMode.release, 'unknown'),
+            splitPerAbi: true,
+            targetArchs: <AndroidArch>[
+                AndroidArch.armeabi_v7a,
+                AndroidArch.arm64_v8a,
+              ]
+            )
+          ).isEmpty, isTrue);
+    });
+    test('should provide bundle file name for default build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.bundleFileFor(BuildInfo.debug), 'app.aab');
+      expect(project.bundleFileFor(BuildInfo.profile), 'app.aab');
+      expect(project.bundleFileFor(BuildInfo.release), 'app.aab');
+      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
+    });
+    test('should provide bundle file name for flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.bundleFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app.aab');
+      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app.aab');
+      expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
+    });
+    test('should provide assemble task name for default build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
+      expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
+      expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
+      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+    });
+    test('should provide assemble task name for flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
+      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
+      expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+    });
+    test('should respect format of the flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
+    });
+    test('bundle should provide assemble task name for default build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
+      expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
+      expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
+      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+    });
+    test('bundle should provide assemble task name for flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
+      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
+      expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
+    });
+    test('bundle should respect format of the flavored build types', () {
+      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
+    });
+  });
+
+  group('Gradle local.properties', () {
+    MockLocalEngineArtifacts mockArtifacts;
+    MockProcessManager mockProcessManager;
+    FakePlatform android;
+    FileSystem fs;
+
+    setUp(() {
+      fs = MemoryFileSystem();
+      mockArtifacts = MockLocalEngineArtifacts();
+      mockProcessManager = MockProcessManager();
+      android = fakePlatform('android');
+    });
+
+    void testUsingAndroidContext(String description, dynamic testMethod()) {
+      testUsingContext(description, testMethod, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        ProcessManager: () => mockProcessManager,
+        Platform: () => android,
+        FileSystem: () => fs,
+      });
+    }
+
+    String propertyFor(String key, File file) {
+      final Iterable<String> result = file.readAsLinesSync()
+          .where((String line) => line.startsWith('$key='))
+          .map((String line) => line.split('=')[1]);
+      return result.isEmpty ? null : result.first;
+    }
+
+    Future<void> checkBuildVersion({
+      String manifest,
+      BuildInfo buildInfo,
+      String expectedBuildName,
+      String expectedBuildNumber,
+    }) async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'android_arm'));
+
+      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
+      manifestFile.createSync(recursive: true);
+      manifestFile.writeAsStringSync(manifest);
+
+      // write schemaData otherwise pubspec.yaml file can't be loaded
+      writeEmptySchemaFile(fs);
+
+      updateLocalProperties(
+        project: FlutterProject.fromPath('path/to/project'),
+        buildInfo: buildInfo,
+        requireAndroidSdk: false,
+      );
+
+      final File localPropertiesFile = fs.file('path/to/project/android/local.properties');
+      expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName);
+      expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber);
+    }
+
+    testUsingAndroidContext('extract build name and number from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingAndroidContext('extract build name from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: null,
+      );
+    });
+
+    testUsingAndroidContext('allow build info to override build name', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingAndroidContext('allow build info to override build number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingAndroidContext('allow build info to override build name and number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingAndroidContext('allow build info to override build name and set number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingAndroidContext('allow build info to set build name and number', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingAndroidContext('allow build info to unset build name and number', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3'),
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4'),
+        expectedBuildName: '1.0.3',
+        expectedBuildNumber: '4',
+      );
+      // Values don't get unset.
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: null,
+        expectedBuildName: '1.0.3',
+        expectedBuildNumber: '4',
+      );
+      // Values get unset.
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null),
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+    });
+  });
+}
+
+Platform fakePlatform(String name) {
+  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
+}
+
+class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
+class MockGradleProject extends Mock implements GradleProject {}
diff --git a/packages/flutter_tools/test/general.shard/application_package_test.dart b/packages/flutter_tools/test/general.shard/application_package_test.dart
new file mode 100644
index 0000000..620ee3f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/application_package_test.dart
@@ -0,0 +1,601 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io' show ProcessResult;
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/fuchsia/application_package.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
+  Platform: _kNoColorTerminalPlatform,
+};
+
+class MockitoProcessManager extends Mock implements ProcessManager {}
+class MockitoAndroidSdk extends Mock implements AndroidSdk {}
+class MockitoAndroidSdkVersion extends Mock implements AndroidSdkVersion {}
+
+void main() {
+  group('Apk with partial Android SDK works', () {
+    AndroidSdk sdk;
+    ProcessManager mockProcessManager;
+    MemoryFileSystem fs;
+    File gradle;
+    final Map<Type, Generator> overrides = <Type, Generator>{
+      AndroidSdk: () => sdk,
+      ProcessManager: () => mockProcessManager,
+      FileSystem: () => fs,
+    };
+
+    setUp(() async {
+      sdk = MockitoAndroidSdk();
+      mockProcessManager = MockitoProcessManager();
+      fs = MemoryFileSystem();
+      Cache.flutterRoot = '../..';
+      when(sdk.licensesAvailable).thenReturn(true);
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+      when(mockProcessManager.run(
+        any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(1, 0, 'stdout', 'stderr'));
+      when(mockProcessManager.runSync(any)).thenReturn(ProcessResult(1, 0, 'stdout', 'stderr'));
+      final FlutterProject project = FlutterProject.current();
+      gradle = fs.file(project.android.hostAppGradleRoot.childFile(
+        platform.isWindows ? 'gradlew.bat' : 'gradlew',
+      ).path)..createSync(recursive: true);
+    });
+
+    testUsingContext('Licenses not available, platform and buildtools available, apk exists', () async {
+      const String aaptPath = 'aaptPath';
+      final File apkFile = fs.file('app.apk');
+      final AndroidSdkVersion sdkVersion = MockitoAndroidSdkVersion();
+      when(sdkVersion.aaptPath).thenReturn(aaptPath);
+      when(sdk.latestVersion).thenReturn(sdkVersion);
+      when(sdk.platformToolsAvailable).thenReturn(true);
+      when(sdk.licensesAvailable).thenReturn(false);
+      when(mockProcessManager.runSync(
+          argThat(equals(<String>[
+            aaptPath,
+            'dump',
+            'xmltree',
+            apkFile.path,
+            'AndroidManifest.xml',
+          ])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ),
+      ).thenReturn(ProcessResult(0, 0, _aaptDataWithDefaultEnabledAndMainLauncherActivity, null));
+
+      final ApplicationPackage applicationPackage = await ApplicationPackageFactory.instance.getPackageForPlatform(
+        TargetPlatform.android_arm,
+        applicationBinary: apkFile,
+      );
+      expect(applicationPackage.name, 'app.apk');
+    }, overrides: overrides);
+
+    testUsingContext('Licenses available, build tools not, apk exists', () async {
+      when(sdk.latestVersion).thenReturn(null);
+      final FlutterProject project = FlutterProject.current();
+      final File gradle = project.android.hostAppGradleRoot.childFile(
+        platform.isWindows ? 'gradlew.bat' : 'gradlew',
+      )..createSync(recursive: true);
+
+      await ApplicationPackageFactory.instance.getPackageForPlatform(
+        TargetPlatform.android_arm,
+        applicationBinary: fs.file('app.apk'),
+      );
+      verify(
+        mockProcessManager.run(
+          argThat(equals(<String>[gradle.path, 'dependencies'])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ),
+      ).called(1);
+    }, overrides: overrides);
+
+    testUsingContext('Licenses available, build tools available, does not call gradle dependencies', () async {
+      final AndroidSdkVersion sdkVersion = MockitoAndroidSdkVersion();
+      when(sdk.latestVersion).thenReturn(sdkVersion);
+
+      await ApplicationPackageFactory.instance.getPackageForPlatform(
+        TargetPlatform.android_arm,
+      );
+      verifyNever(
+        mockProcessManager.run(
+          argThat(equals(<String>[gradle.path, 'dependencies'])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ),
+      );
+    }, overrides: overrides);
+  });
+
+  group('ApkManifestData', () {
+    testUsingContext('Parses manifest with an Activity that has enabled set to true, action set to android.intent.action.MAIN and category set to android.intent.category.LAUNCHER', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithExplicitEnabledAndMainLauncherActivity);
+      expect(data, isNotNull);
+      expect(data.packageName, 'io.flutter.examples.hello_world');
+      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('Parses manifest with an Activity that has no value for its enabled field, action set to android.intent.action.MAIN and category set to android.intent.category.LAUNCHER', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithDefaultEnabledAndMainLauncherActivity);
+      expect(data, isNotNull);
+      expect(data.packageName, 'io.flutter.examples.hello_world');
+      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity2');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('Parses manifest with a dist namespace', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithDistNamespace);
+      expect(data, isNotNull);
+      expect(data.packageName, 'io.flutter.examples.hello_world');
+      expect(data.launchableActivityName, 'io.flutter.examples.hello_world.MainActivity');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('Error when parsing manifest with no Activity that has enabled set to true nor has no value for its enabled field', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithNoEnabledActivity);
+      expect(data, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+          logger.errorText, 'Error running io.flutter.examples.hello_world. Default activity not found\n');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('Error when parsing manifest with no Activity that has action set to android.intent.action.MAIN', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithNoMainActivity);
+      expect(data, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+          logger.errorText, 'Error running io.flutter.examples.hello_world. Default activity not found\n');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('Error when parsing manifest with no Activity that has category set to android.intent.category.LAUNCHER', () {
+      final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithNoLauncherActivity);
+      expect(data, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+          logger.errorText, 'Error running io.flutter.examples.hello_world. Default activity not found\n');
+    }, overrides: noColorTerminalOverride);
+  });
+
+  group('PrebuiltIOSApp', () {
+    final Map<Type, Generator> overrides = <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      IOSWorkflow: () => MockIosWorkFlow(),
+      Platform: _kNoColorTerminalPlatform,
+      OperatingSystemUtils: () => MockOperatingSystemUtils(),
+    };
+
+    testUsingContext('Error on non-existing file', () {
+      final PrebuiltIOSApp iosApp =
+          IOSApp.fromPrebuiltApp(fs.file('not_existing.ipa'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        'File "not_existing.ipa" does not exist. Use an app bundle or an ipa.\n',
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Error on non-app-bundle folder', () {
+      fs.directory('regular_folder').createSync();
+      final PrebuiltIOSApp iosApp =
+          IOSApp.fromPrebuiltApp(fs.file('regular_folder'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+          logger.errorText, 'Folder "regular_folder" is not an app bundle.\n');
+    }, overrides: overrides);
+
+    testUsingContext('Error on no info.plist', () {
+      fs.directory('bundle.app').createSync();
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('bundle.app'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        'Invalid prebuilt iOS app. Does not contain Info.plist.\n',
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Error on bad info.plist', () {
+      fs.directory('bundle.app').createSync();
+      fs.file('bundle.app/Info.plist').writeAsStringSync(badPlistData);
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('bundle.app'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        contains(
+            'Invalid prebuilt iOS app. Info.plist does not contain bundle identifier\n'),
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Success with app bundle', () {
+      fs.directory('bundle.app').createSync();
+      fs.file('bundle.app/Info.plist').writeAsStringSync(plistData);
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('bundle.app'));
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText, isEmpty);
+      expect(iosApp.bundleDir.path, 'bundle.app');
+      expect(iosApp.id, 'fooBundleId');
+      expect(iosApp.bundleName, 'bundle.app');
+    }, overrides: overrides);
+
+    testUsingContext('Bad ipa zip-file, no payload dir', () {
+      fs.file('app.ipa').createSync();
+      when(os.unzip(fs.file('app.ipa'), any)).thenAnswer((Invocation _) { });
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('app.ipa'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.\n',
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Bad ipa zip-file, two app bundles', () {
+      fs.file('app.ipa').createSync();
+      when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
+        final File zipFile = invocation.positionalArguments[0];
+        if (zipFile.path != 'app.ipa') {
+          return null;
+        }
+        final Directory targetDirectory = invocation.positionalArguments[1];
+        final String bundlePath1 =
+            fs.path.join(targetDirectory.path, 'Payload', 'bundle1.app');
+        final String bundlePath2 =
+            fs.path.join(targetDirectory.path, 'Payload', 'bundle2.app');
+        fs.directory(bundlePath1).createSync(recursive: true);
+        fs.directory(bundlePath2).createSync(recursive: true);
+      });
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('app.ipa'));
+      expect(iosApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+          'Invalid prebuilt iOS ipa. Does not contain a single app bundle.\n');
+    }, overrides: overrides);
+
+    testUsingContext('Success with ipa', () {
+      fs.file('app.ipa').createSync();
+      when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
+        final File zipFile = invocation.positionalArguments[0];
+        if (zipFile.path != 'app.ipa') {
+          return null;
+        }
+        final Directory targetDirectory = invocation.positionalArguments[1];
+        final Directory bundleAppDir = fs.directory(
+            fs.path.join(targetDirectory.path, 'Payload', 'bundle.app'));
+        bundleAppDir.createSync(recursive: true);
+        fs
+            .file(fs.path.join(bundleAppDir.path, 'Info.plist'))
+            .writeAsStringSync(plistData);
+      });
+      final PrebuiltIOSApp iosApp = IOSApp.fromPrebuiltApp(fs.file('app.ipa'));
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText, isEmpty);
+      expect(iosApp.bundleDir.path, endsWith('bundle.app'));
+      expect(iosApp.id, 'fooBundleId');
+      expect(iosApp.bundleName, 'bundle.app');
+    }, overrides: overrides);
+
+    testUsingContext('returns null when there is no ios or .ios directory', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      final BuildableIOSApp iosApp = IOSApp.fromIosProject(FlutterProject.fromDirectory(fs.currentDirectory).ios);
+
+      expect(iosApp, null);
+    }, overrides: overrides);
+
+    testUsingContext('returns null when there is no Runner.xcodeproj', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      fs.file('ios/FooBar.xcodeproj').createSync(recursive: true);
+      final BuildableIOSApp iosApp = IOSApp.fromIosProject(FlutterProject.fromDirectory(fs.currentDirectory).ios);
+
+      expect(iosApp, null);
+    }, overrides: overrides);
+
+    testUsingContext('returns null when there is no Runner.xcodeproj/project.pbxproj', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      fs.file('ios/Runner.xcodeproj').createSync(recursive: true);
+      final BuildableIOSApp iosApp = IOSApp.fromIosProject(FlutterProject.fromDirectory(fs.currentDirectory).ios);
+
+      expect(iosApp, null);
+    }, overrides: overrides);
+  });
+
+  group('FuchsiaApp', () {
+    final Map<Type, Generator> overrides = <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Platform: _kNoColorTerminalPlatform,
+      OperatingSystemUtils: () => MockOperatingSystemUtils(),
+    };
+
+    testUsingContext('Error on non-existing file', () {
+      final PrebuiltFuchsiaApp fuchsiaApp =
+          FuchsiaApp.fromPrebuiltApp(fs.file('not_existing.far'));
+      expect(fuchsiaApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        'File "not_existing.far" does not exist or is not a .far file. Use far archive.\n',
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Error on non-far file', () {
+      fs.directory('regular_folder').createSync();
+      final PrebuiltFuchsiaApp fuchsiaApp =
+          FuchsiaApp.fromPrebuiltApp(fs.file('regular_folder'));
+      expect(fuchsiaApp, isNull);
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.errorText,
+        'File "regular_folder" does not exist or is not a .far file. Use far archive.\n',
+      );
+    }, overrides: overrides);
+
+    testUsingContext('Success with far file', () {
+      fs.file('bundle.far').createSync();
+      final PrebuiltFuchsiaApp fuchsiaApp = FuchsiaApp.fromPrebuiltApp(fs.file('bundle.far'));
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText, isEmpty);
+      expect(fuchsiaApp.id, 'bundle.far');
+    }, overrides: overrides);
+
+    testUsingContext('returns null when there is no fuchsia', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      final BuildableFuchsiaApp fuchsiaApp = FuchsiaApp.fromFuchsiaProject(FlutterProject.fromDirectory(fs.currentDirectory).fuchsia);
+
+      expect(fuchsiaApp, null);
+    }, overrides: overrides);
+  });
+}
+
+const String _aaptDataWithExplicitEnabledAndMainLauncherActivity =
+'''N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=7)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x1
+    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
+    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+    E: uses-sdk (line=12)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
+    E: uses-permission (line=21)
+      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+    E: application (line=29)
+      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+      A: android:icon(0x01010002)=@0x7f010000
+      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
+      E: activity (line=34)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+        A: android:enabled(0x0101000e)=(type 0x12)0x0
+        A: android:launchMode(0x0101001d)=(type 0x10)0x1
+        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
+        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+        E: intent-filter (line=42)
+          E: action (line=43)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+          E: category (line=45)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
+      E: activity (line=48)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:label(0x01010001)="app2" (Raw: "app2")
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
+        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
+        E: intent-filter (line=53)
+          E: action (line=54)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+          E: category (line=56)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
+
+
+const String _aaptDataWithDefaultEnabledAndMainLauncherActivity =
+'''N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=7)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x1
+    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
+    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+    E: uses-sdk (line=12)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
+    E: uses-permission (line=21)
+      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+    E: application (line=29)
+      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+      A: android:icon(0x01010002)=@0x7f010000
+      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
+      E: activity (line=34)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+        A: android:enabled(0x0101000e)=(type 0x12)0x0
+        A: android:launchMode(0x0101001d)=(type 0x10)0x1
+        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
+        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+        E: intent-filter (line=42)
+          E: action (line=43)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+          E: category (line=45)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
+      E: activity (line=48)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:label(0x01010001)="app2" (Raw: "app2")
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity2" (Raw: "io.flutter.examples.hello_world.MainActivity2")
+        E: intent-filter (line=53)
+          E: action (line=54)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+          E: category (line=56)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
+
+
+const String _aaptDataWithNoEnabledActivity =
+'''N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=7)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x1
+    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
+    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+    E: uses-sdk (line=12)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
+    E: uses-permission (line=21)
+      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+    E: application (line=29)
+      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+      A: android:icon(0x01010002)=@0x7f010000
+      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
+      E: activity (line=34)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+        A: android:enabled(0x0101000e)=(type 0x12)0x0
+        A: android:launchMode(0x0101001d)=(type 0x10)0x1
+        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
+        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+        E: intent-filter (line=42)
+          E: action (line=43)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+          E: category (line=45)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
+
+const String _aaptDataWithNoMainActivity =
+'''N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=7)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x1
+    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
+    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+    E: uses-sdk (line=12)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
+    E: uses-permission (line=21)
+      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+    E: application (line=29)
+      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+      A: android:icon(0x01010002)=@0x7f010000
+      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
+      E: activity (line=34)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
+        A: android:launchMode(0x0101001d)=(type 0x10)0x1
+        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
+        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+        E: intent-filter (line=42)
+          E: category (line=43)
+            A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")''';
+
+const String _aaptDataWithNoLauncherActivity =
+'''N: android=http://schemas.android.com/apk/res/android
+  E: manifest (line=7)
+    A: android:versionCode(0x0101021b)=(type 0x10)0x1
+    A: android:versionName(0x0101021c)="0.0.1" (Raw: "0.0.1")
+    A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+    E: uses-sdk (line=12)
+      A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+      A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1b
+    E: uses-permission (line=21)
+      A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+    E: application (line=29)
+      A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+      A: android:icon(0x01010002)=@0x7f010000
+      A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+      A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
+      E: activity (line=34)
+        A: android:theme(0x01010000)=@0x1030009
+        A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+        A: android:enabled(0x0101000e)=(type 0x12)0xffffffff
+        A: android:launchMode(0x0101001d)=(type 0x10)0x1
+        A: android:configChanges(0x0101001f)=(type 0x11)0x400035b4
+        A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+        A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+        E: intent-filter (line=42)
+          E: action (line=43)
+            A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")''';
+
+const String _aaptDataWithDistNamespace =
+'''N: android=http://schemas.android.com/apk/res/android
+  N: dist=http://schemas.android.com/apk/distribution
+    E: manifest (line=7)
+      A: android:versionCode(0x0101021b)=(type 0x10)0x1
+      A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
+      A: android:compileSdkVersion(0x01010572)=(type 0x10)0x1c
+      A: android:compileSdkVersionCodename(0x01010573)="9" (Raw: "9")
+      A: package="io.flutter.examples.hello_world" (Raw: "io.flutter.examples.hello_world")
+      A: platformBuildVersionCode=(type 0x10)0x1
+      A: platformBuildVersionName=(type 0x4)0x3f800000
+      E: uses-sdk (line=13)
+        A: android:minSdkVersion(0x0101020c)=(type 0x10)0x10
+        A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
+      E: dist:module (line=17)
+        A: dist:instant=(type 0x12)0xffffffff
+      E: uses-permission (line=24)
+        A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
+      E: application (line=32)
+        A: android:label(0x01010001)="hello_world" (Raw: "hello_world")
+        A: android:icon(0x01010002)=@0x7f010000
+        A: android:name(0x01010003)="io.flutter.app.FlutterApplication" (Raw: "io.flutter.app.FlutterApplication")
+        E: activity (line=36)
+          A: android:theme(0x01010000)=@0x01030009
+          A: android:name(0x01010003)="io.flutter.examples.hello_world.MainActivity" (Raw: "io.flutter.examples.hello_world.MainActivity")
+          A: android:launchMode(0x0101001d)=(type 0x10)0x1
+          A: android:configChanges(0x0101001f)=(type 0x11)0x400037b4
+          A: android:windowSoftInputMode(0x0101022b)=(type 0x11)0x10
+          A: android:hardwareAccelerated(0x010102d3)=(type 0x12)0xffffffff
+          E: intent-filter (line=43)
+            E: action (line=44)
+              A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
+            E: category (line=46)
+              A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
+''';
+
+
+class MockIosWorkFlow extends Mock implements IOSWorkflow {
+  @override
+  String getPlistValueFromFile(String path, String key) {
+    final File file = fs.file(path);
+    if (!file.existsSync()) {
+      return null;
+    }
+    return json.decode(file.readAsStringSync())[key];
+  }
+}
+
+// Contains no bundle identifier.
+const String badPlistData = '''
+{}
+''';
+
+const String plistData = '''
+{"CFBundleIdentifier": "fooBundleId"}
+''';
+
+class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { }
diff --git a/packages/flutter_tools/test/general.shard/artifacts_test.dart b/packages/flutter_tools/test/general.shard/artifacts_test.dart
new file mode 100644
index 0000000..fc522c8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/artifacts_test.dart
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('CachedArtifacts', () {
+
+    Directory tempDir;
+    CachedArtifacts artifacts;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_artifacts_test_cached.');
+      artifacts = CachedArtifacts();
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('getArtifactPath', () {
+      expect(
+          artifacts.getArtifactPath(Artifact.flutterFramework, platform: TargetPlatform.ios, mode: BuildMode.release),
+          fs.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'engine', 'ios-release', 'Flutter.framework'),
+      );
+      expect(
+          artifacts.getArtifactPath(Artifact.flutterTester),
+          fs.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'engine', 'linux-x64', 'flutter_tester'),
+      );
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+
+    testUsingContext('getEngineType', () {
+      expect(
+          artifacts.getEngineType(TargetPlatform.android_arm, BuildMode.debug),
+          'android-arm',
+      );
+      expect(
+          artifacts.getEngineType(TargetPlatform.ios, BuildMode.release),
+          'ios-release',
+      );
+      expect(
+          artifacts.getEngineType(TargetPlatform.darwin_x64),
+          'darwin-x64',
+      );
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+  });
+
+  group('LocalEngineArtifacts', () {
+
+    Directory tempDir;
+    LocalEngineArtifacts artifacts;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_artifacts_test_local.');
+      artifacts = LocalEngineArtifacts(tempDir.path,
+        fs.path.join(tempDir.path, 'out', 'android_debug_unopt'),
+        fs.path.join(tempDir.path, 'out', 'host_debug_unopt'),
+      );
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('getArtifactPath', () {
+      expect(
+          artifacts.getArtifactPath(Artifact.flutterFramework, platform: TargetPlatform.ios, mode: BuildMode.release),
+          fs.path.join(tempDir.path, 'out', 'android_debug_unopt', 'Flutter.framework'),
+      );
+      expect(
+          artifacts.getArtifactPath(Artifact.flutterTester),
+          fs.path.join(tempDir.path, 'out', 'android_debug_unopt', 'flutter_tester'),
+      );
+      expect(
+        artifacts.getArtifactPath(Artifact.engineDartSdkPath),
+        fs.path.join(tempDir.path, 'out', 'host_debug_unopt', 'dart-sdk'),
+      );
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+
+    testUsingContext('getEngineType', () {
+      expect(
+          artifacts.getEngineType(TargetPlatform.android_arm, BuildMode.debug),
+          'android_debug_unopt',
+      );
+      expect(
+          artifacts.getEngineType(TargetPlatform.ios, BuildMode.release),
+          'android_debug_unopt',
+      );
+      expect(
+          artifacts.getEngineType(TargetPlatform.darwin_x64),
+          'android_debug_unopt',
+      );
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart
new file mode 100644
index 0000000..9e50586
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/asset_bundle_package_fonts_test.dart
@@ -0,0 +1,332 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+
+import 'package:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/pubspec_schema.dart';
+
+void main() {
+  String fixPath(String path) {
+    // The in-memory file system is strict about slashes on Windows being the
+    // correct way so until https://github.com/google/file.dart/issues/112 is
+    // fixed we fix them here.
+    // TODO(dantup): Remove this function once the above issue is fixed and
+    // rolls into Flutter.
+    return path?.replaceAll('/', fs.path.separator);
+  }
+  void writePubspecFile(String path, String name, { String fontsSection }) {
+    if (fontsSection == null) {
+      fontsSection = '';
+    } else {
+      fontsSection = '''
+flutter:
+     fonts:
+$fontsSection
+''';
+    }
+
+    fs.file(fixPath(path))
+      ..createSync(recursive: true)
+      ..writeAsStringSync('''
+name: $name
+dependencies:
+  flutter:
+    sdk: flutter
+$fontsSection
+''');
+  }
+
+  void establishFlutterRoot() {
+    // Setting flutterRoot here so that it picks up the MemoryFileSystem's
+    // path separator.
+    Cache.flutterRoot = getFlutterRoot();
+  }
+
+  void writePackagesFile(String packages) {
+    fs.file('.packages')
+      ..createSync()
+      ..writeAsStringSync(packages);
+  }
+
+  Future<void> buildAndVerifyFonts(
+    List<String> localFonts,
+    List<String> packageFonts,
+    List<String> packages,
+    String expectedAssetManifest,
+  ) async {
+    final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+    await bundle.build(manifestPath: 'pubspec.yaml');
+
+    for (String packageName in packages) {
+      for (String packageFont in packageFonts) {
+        final String entryKey = 'packages/$packageName/$packageFont';
+        expect(bundle.entries.containsKey(entryKey), true);
+        expect(
+          utf8.decode(await bundle.entries[entryKey].contentsAsBytes()),
+          packageFont,
+        );
+      }
+
+      for (String localFont in localFonts) {
+        expect(bundle.entries.containsKey(localFont), true);
+        expect(
+          utf8.decode(await bundle.entries[localFont].contentsAsBytes()),
+          localFont,
+        );
+      }
+    }
+
+    expect(
+      json.decode(utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes())),
+      json.decode(expectedAssetManifest),
+    );
+  }
+
+  void writeFontAsset(String path, String font) {
+    fs.file(fixPath('$path$font'))
+      ..createSync(recursive: true)
+      ..writeAsStringSync(font);
+  }
+
+  group('AssetBundle fonts from packages', () {
+    FileSystem testFileSystem;
+
+    setUp(() async {
+      testFileSystem = MemoryFileSystem(
+        style: platform.isWindows
+          ? FileSystemStyle.windows
+          : FileSystemStyle.posix,
+      );
+      testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
+    });
+
+    testUsingContext('App includes neither font manifest nor fonts when no defines fonts', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
+      expect(bundle.entries.containsKey('FontManifest.json'), isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App font uses font file from package', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      const String fontsSection = '''
+       - family: foo
+         fonts:
+           - asset: packages/test_package/bar
+''';
+      writePubspecFile('pubspec.yaml', 'test', fontsSection: fontsSection);
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      const String font = 'bar';
+      writeFontAsset('p/p/lib/', font);
+
+      const String expectedFontManifest =
+          '[{"fonts":[{"asset":"packages/test_package/bar"}],"family":"foo"}]';
+      await buildAndVerifyFonts(
+        <String>[],
+        <String>[font],
+        <String>['test_package'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App font uses local font file and package font file', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      const String fontsSection = '''
+       - family: foo
+         fonts:
+           - asset: packages/test_package/bar
+           - asset: a/bar
+''';
+      writePubspecFile('pubspec.yaml', 'test', fontsSection: fontsSection);
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      const String packageFont = 'bar';
+      writeFontAsset('p/p/lib/', packageFont);
+      const String localFont = 'a/bar';
+      writeFontAsset('', localFont);
+
+      const String expectedFontManifest =
+          '[{"fonts":[{"asset":"packages/test_package/bar"},{"asset":"a/bar"}],'
+          '"family":"foo"}]';
+      await buildAndVerifyFonts(
+        <String>[localFont],
+        <String>[packageFont],
+        <String>['test_package'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App uses package font with own font file', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      const String fontsSection = '''
+       - family: foo
+         fonts:
+           - asset: a/bar
+''';
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        fontsSection: fontsSection,
+      );
+
+      const String font = 'a/bar';
+      writeFontAsset('p/p/', font);
+
+      const String expectedFontManifest =
+          '[{"family":"packages/test_package/foo",'
+          '"fonts":[{"asset":"packages/test_package/a/bar"}]}]';
+      await buildAndVerifyFonts(
+        <String>[],
+        <String>[font],
+        <String>['test_package'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App uses package font with font file from another package', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
+      const String fontsSection = '''
+       - family: foo
+         fonts:
+           - asset: packages/test_package2/bar
+''';
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        fontsSection: fontsSection,
+      );
+      writePubspecFile('p2/p/pubspec.yaml', 'test_package2');
+
+      const String font = 'bar';
+      writeFontAsset('p2/p/lib/', font);
+
+      const String expectedFontManifest =
+          '[{"family":"packages/test_package/foo",'
+          '"fonts":[{"asset":"packages/test_package2/bar"}]}]';
+      await buildAndVerifyFonts(
+        <String>[],
+        <String>[font],
+        <String>['test_package2'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App uses package font with properties and own font file', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      const String pubspec = '''
+       - family: foo
+         fonts:
+           - style: italic
+             weight: 400
+             asset: a/bar
+''';
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        fontsSection: pubspec,
+      );
+      const String font = 'a/bar';
+      writeFontAsset('p/p/', font);
+
+      const String expectedFontManifest =
+          '[{"family":"packages/test_package/foo",'
+          '"fonts":[{"weight":400,"style":"italic","asset":"packages/test_package/a/bar"}]}]';
+      await buildAndVerifyFonts(
+        <String>[],
+        <String>[font],
+        <String>['test_package'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('App uses local font and package font with own font file.', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      const String fontsSection = '''
+       - family: foo
+         fonts:
+           - asset: a/bar
+''';
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+        fontsSection: fontsSection,
+      );
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        fontsSection: fontsSection,
+      );
+
+      const String font = 'a/bar';
+      writeFontAsset('', font);
+      writeFontAsset('p/p/', font);
+
+      const String expectedFontManifest =
+          '[{"fonts":[{"asset":"a/bar"}],"family":"foo"},'
+          '{"family":"packages/test_package/foo",'
+          '"fonts":[{"asset":"packages/test_package/a/bar"}]}]';
+      await buildAndVerifyFonts(
+        <String>[font],
+        <String>[font],
+        <String>['test_package'],
+        expectedFontManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart
new file mode 100644
index 0000000..4c48cda
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/asset_bundle_package_test.dart
@@ -0,0 +1,670 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+
+import 'package:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/pubspec_schema.dart';
+
+void main() {
+  String fixPath(String path) {
+    // The in-memory file system is strict about slashes on Windows being the
+    // correct way so until https://github.com/google/file.dart/issues/112 is
+    // fixed we fix them here.
+    // TODO(dantup): Remove this function once the above issue is fixed and
+    // rolls into Flutter.
+    return path?.replaceAll('/', fs.path.separator);
+  }
+  void writePubspecFile(String path, String name, { List<String> assets }) {
+    String assetsSection;
+    if (assets == null) {
+      assetsSection = '';
+    } else {
+      final StringBuffer buffer = StringBuffer();
+      buffer.write('''
+flutter:
+     assets:
+''');
+
+      for (String asset in assets) {
+        buffer.write('''
+       - $asset
+''');
+      }
+      assetsSection = buffer.toString();
+    }
+
+    fs.file(fixPath(path))
+      ..createSync(recursive: true)
+      ..writeAsStringSync('''
+name: $name
+dependencies:
+  flutter:
+    sdk: flutter
+$assetsSection
+''');
+  }
+
+  void establishFlutterRoot() {
+    Cache.flutterRoot = getFlutterRoot();
+  }
+
+  void writePackagesFile(String packages) {
+    fs.file('.packages')
+      ..createSync()
+      ..writeAsStringSync(packages);
+  }
+
+  Future<void> buildAndVerifyAssets(
+    List<String> assets,
+    List<String> packages,
+    String expectedAssetManifest,
+  ) async {
+    final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+    await bundle.build(manifestPath: 'pubspec.yaml');
+
+    for (String packageName in packages) {
+      for (String asset in assets) {
+        final String entryKey = Uri.encodeFull('packages/$packageName/$asset');
+        expect(bundle.entries.containsKey(entryKey), true, reason: 'Cannot find key on bundle: $entryKey');
+        expect(
+          utf8.decode(await bundle.entries[entryKey].contentsAsBytes()),
+          asset,
+        );
+      }
+    }
+
+    expect(
+      utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
+      expectedAssetManifest,
+    );
+  }
+
+  void writeAssets(String path, List<String> assets) {
+    for (String asset in assets) {
+      final String fullPath = fixPath(fs.path.join(path, asset));
+
+      fs.file(fullPath)
+        ..createSync(recursive: true)
+        ..writeAsStringSync(asset);
+    }
+  }
+
+  FileSystem testFileSystem;
+
+  setUp(() async {
+    testFileSystem = MemoryFileSystem(
+      style: platform.isWindows
+        ? FileSystemStyle.windows
+        : FileSystemStyle.posix,
+    );
+    testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
+  });
+
+  group('AssetBundle assets from packages', () {
+    testUsingContext('No assets are bundled when the package has no assets', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
+      const String expectedAssetManifest = '{}';
+      expect(
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        expectedAssetManifest,
+      );
+      expect(
+        utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()),
+        '[]',
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('No assets are bundled when the package has an asset that is not listed', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      final List<String> assets = <String>['a/foo'];
+      writeAssets('p/p/', assets);
+
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
+      const String expectedAssetManifest = '{}';
+      expect(
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        expectedAssetManifest,
+      );
+      expect(
+        utf8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()),
+        '[]',
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('One asset is bundled when the package has and lists one asset its pubspec', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assets = <String>['a/foo'];
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assets,
+      );
+
+      writeAssets('p/p/', assets);
+
+      const String expectedAssetManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo"]}';
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext("One asset is bundled when the package has one asset, listed in the app's pubspec", () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      final List<String> assetEntries = <String>['packages/test_package/a/foo'];
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+        assets: assetEntries,
+      );
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile('p/p/pubspec.yaml', 'test_package');
+
+      final List<String> assets = <String>['a/foo'];
+      writeAssets('p/p/lib/', assets);
+
+      const String expectedAssetManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo"]}';
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('One asset and its variant are bundled when the package has an asset and a variant, and lists the asset in its pubspec', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: <String>['a/foo'],
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/', assets);
+
+      const String expectedManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('One asset and its variant are bundled when the package has an asset and a variant, and the app lists the asset in its pubspec', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+        assets: <String>['packages/test_package/a/foo'],
+      );
+      writePackagesFile('test_package:p/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/lib/', assets);
+
+      const String expectedManifest = '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('Two assets are bundled when the package has and lists two assets in its pubspec', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assets = <String>['a/foo', 'a/bar'];
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assets,
+      );
+
+      writeAssets('p/p/', assets);
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
+          '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext("Two assets are bundled when the package has two assets, listed in the app's pubspec", () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      final List<String> assetEntries = <String>[
+        'packages/test_package/a/foo',
+        'packages/test_package/a/bar',
+      ];
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+         assets: assetEntries,
+      );
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assets = <String>['a/foo', 'a/bar'];
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+      );
+
+      writeAssets('p/p/lib/', assets);
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
+          '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('Two assets are bundled when two packages each have and list an asset their pubspec', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+      );
+      writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: <String>['a/foo'],
+      );
+      writePubspecFile(
+        'p2/p/pubspec.yaml',
+        'test_package2',
+        assets: <String>['a/foo'],
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/', assets);
+      writeAssets('p2/p/', assets);
+
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"],'
+          '"packages/test_package2/a/foo":'
+          '["packages/test_package2/a/foo","packages/test_package2/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package', 'test_package2'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext("Two assets are bundled when two packages each have an asset, listed in the app's pubspec", () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      final List<String> assetEntries = <String>[
+        'packages/test_package/a/foo',
+        'packages/test_package2/a/foo',
+      ];
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+        assets: assetEntries,
+      );
+      writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+      );
+      writePubspecFile(
+        'p2/p/pubspec.yaml',
+        'test_package2',
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p/p/lib/', assets);
+      writeAssets('p2/p/lib/', assets);
+
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/foo":'
+          '["packages/test_package/a/foo","packages/test_package/a/v/foo"],'
+          '"packages/test_package2/a/foo":'
+          '["packages/test_package2/a/foo","packages/test_package2/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package', 'test_package2'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('One asset is bundled when the app depends on a package, listing in its pubspec an asset from another package', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+      writePubspecFile(
+        'pubspec.yaml',
+        'test',
+      );
+      writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: <String>['packages/test_package2/a/foo'],
+      );
+      writePubspecFile(
+        'p2/p/pubspec.yaml',
+        'test_package2',
+      );
+
+      final List<String> assets = <String>['a/foo', 'a/v/foo'];
+      writeAssets('p2/p/lib/', assets);
+
+      const String expectedAssetManifest =
+          '{"packages/test_package2/a/foo":'
+          '["packages/test_package2/a/foo","packages/test_package2/a/v/foo"]}';
+
+      await buildAndVerifyAssets(
+        assets,
+        <String>['test_package2'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+  });
+
+  testUsingContext('Asset paths can contain URL reserved characters', () async {
+    establishFlutterRoot();
+    writeEmptySchemaFile(fs);
+
+    writePubspecFile('pubspec.yaml', 'test');
+    writePackagesFile('test_package:p/p/lib/');
+
+    final List<String> assets = <String>['a/foo', 'a/foo[x]'];
+    writePubspecFile(
+      'p/p/pubspec.yaml',
+      'test_package',
+      assets: assets,
+    );
+
+    writeAssets('p/p/', assets);
+    const String expectedAssetManifest =
+        '{"packages/test_package/a/foo":["packages/test_package/a/foo"],'
+        '"packages/test_package/a/foo%5Bx%5D":["packages/test_package/a/foo%5Bx%5D"]}';
+
+    await buildAndVerifyAssets(
+      assets,
+      <String>['test_package'],
+      expectedAssetManifest,
+    );
+  }, overrides: <Type, Generator>{
+    FileSystem: () => testFileSystem,
+  });
+
+  group('AssetBundle assets from scanned paths', () {
+    testUsingContext(
+        'Two assets are bundled when scanning their directory', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetsOnDisk = <String>['a/foo', 'a/bar'];
+      final List<String> assetsOnManifest = <String>['a/'];
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetsOnManifest,
+      );
+
+      writeAssets('p/p/', assetsOnDisk);
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/bar":["packages/test_package/a/bar"],'
+          '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
+
+      await buildAndVerifyAssets(
+        assetsOnDisk,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext(
+        'Two assets are bundled when listing one and scanning second directory', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetsOnDisk = <String>['a/foo', 'abc/bar'];
+      final List<String> assetOnManifest = <String>['a/foo', 'abc/'];
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetOnManifest,
+      );
+
+      writeAssets('p/p/', assetsOnDisk);
+      const String expectedAssetManifest =
+          '{"packages/test_package/abc/bar":["packages/test_package/abc/bar"],'
+          '"packages/test_package/a/foo":["packages/test_package/a/foo"]}';
+
+      await buildAndVerifyAssets(
+        assetsOnDisk,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext(
+        'One asset is bundled with variant, scanning wrong directory', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetsOnDisk = <String>['a/foo','a/b/foo','a/bar'];
+      final List<String> assetOnManifest = <String>['a','a/bar']; // can't list 'a' as asset, should be 'a/'
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetOnManifest,
+      );
+
+      writeAssets('p/p/', assetsOnDisk);
+
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      assert(bundle.entries['AssetManifest.json'] == null,'Invalid pubspec.yaml should not generate AssetManifest.json'  );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+  });
+
+  group('AssetBundle assets from scanned paths with MemoryFileSystem', () {
+    testUsingContext(
+        'One asset is bundled with variant, scanning directory', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetsOnDisk = <String>['a/foo','a/b/foo'];
+      final List<String> assetOnManifest = <String>['a/',];
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetOnManifest,
+      );
+
+      writeAssets('p/p/', assetsOnDisk);
+      const String expectedAssetManifest =
+          '{"packages/test_package/a/foo":["packages/test_package/a/foo","packages/test_package/a/b/foo"]}';
+
+      await buildAndVerifyAssets(
+        assetsOnDisk,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext(
+        'No asset is bundled with variant, no assets or directories are listed', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetsOnDisk = <String>['a/foo', 'a/b/foo'];
+      final List<String> assetOnManifest = <String>[];
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetOnManifest,
+      );
+
+      writeAssets('p/p/', assetsOnDisk);
+      const String expectedAssetManifest = '{}';
+
+      await buildAndVerifyAssets(
+        assetOnManifest,
+        <String>['test_package'],
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext(
+        'Expect error generating manifest, wrong non-existing directory is listed', () async {
+      establishFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      writePubspecFile('pubspec.yaml', 'test');
+      writePackagesFile('test_package:p/p/lib/');
+
+      final List<String> assetOnManifest = <String>['c/'];
+
+      writePubspecFile(
+        'p/p/pubspec.yaml',
+        'test_package',
+        assets: assetOnManifest,
+      );
+
+      try {
+        await buildAndVerifyAssets(
+          assetOnManifest,
+          <String>['test_package'],
+          null,
+        );
+
+        final Function watchdog = () async {
+          assert(false, 'Code failed to detect missing directory. Test failed.');
+        };
+        watchdog();
+      } catch (e) {
+        // Test successful
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart
new file mode 100644
index 0000000..3ffd01f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart
@@ -0,0 +1,145 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io' as io;
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+
+import 'package:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  setUpAll(() {
+    Cache.flutterRoot = getFlutterRoot();
+  });
+
+  group('AssetBundle.build', () {
+    FileSystem testFileSystem;
+
+    setUp(() async {
+      testFileSystem = MemoryFileSystem(
+        style: platform.isWindows
+          ? FileSystemStyle.windows
+          : FileSystemStyle.posix,
+      );
+      testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_test.');
+    });
+
+    testUsingContext('nonempty', () async {
+      final AssetBundle ab = AssetBundleFactory.instance.createBundle();
+      expect(await ab.build(), 0);
+      expect(ab.entries.length, greaterThan(0));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('empty pubspec', () async {
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync('');
+
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      expect(bundle.entries.length, 1);
+      const String expectedAssetManifest = '{}';
+      expect(
+        utf8.decode(await bundle.entries['AssetManifest.json'].contentsAsBytes()),
+        expectedAssetManifest,
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('wildcard directories are updated when filesystem changes', () async {
+      fs.file('.packages').createSync();
+      fs.file(fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true);
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+name: example
+flutter:
+  assets:
+    - assets/foo/
+''');
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      expect(bundle.entries.length, 4);
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), false);
+
+      // Adding a file should update the stat of the directory, but instead
+      // we need to fully recreate it.
+      fs.directory(fs.path.join('assets', 'foo')).deleteSync(recursive: true);
+      fs.file(fs.path.join('assets', 'foo', 'fizz.txt')).createSync(recursive: true);
+      fs.file(fs.path.join('assets', 'foo', 'bar.txt')).createSync();
+
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), true);
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      //  - assets/foo/fizz.txt
+      expect(bundle.entries.length, 5);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('handle removal of wildcard directories', () async {
+      fs.file('.packages').createSync();
+      fs.file(fs.path.join('assets', 'foo', 'bar.txt')).createSync(recursive: true);
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+name: example
+flutter:
+  assets:
+    - assets/foo/
+''');
+      final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      expect(bundle.entries.length, 4);
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), false);
+
+      // Delete the wildcard directory and update pubspec file.
+      fs.directory(fs.path.join('assets', 'foo')).deleteSync(recursive: true);
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+name: example''');
+
+      // Even though the previous file was removed, it is left in the
+      // asset manifest and not updated. This is due to the devfs not
+      // supporting file deletion.
+      expect(bundle.needsBuild(manifestPath: 'pubspec.yaml'), true);
+      await bundle.build(manifestPath: 'pubspec.yaml');
+      // Expected assets:
+      //  - asset manifest
+      //  - font manifest
+      //  - license file
+      //  - assets/foo/bar.txt
+      expect(bundle.entries.length, 4);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    }, skip: io.Platform.isWindows /* https://github.com/flutter/flutter/issues/34446 */);
+  });
+
+}
diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart
new file mode 100644
index 0000000..a2b37f1
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/asset_bundle_variant_test.dart
@@ -0,0 +1,97 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+
+import 'package:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/pubspec_schema.dart';
+
+void main() {
+  String fixPath(String path) {
+    // The in-memory file system is strict about slashes on Windows being the
+    // correct way so until https://github.com/google/file.dart/issues/112 is
+    // fixed we fix them here.
+    // TODO(dantup): Remove this function once the above issue is fixed and
+    // rolls into Flutter.
+    return path?.replaceAll('/', fs.path.separator);
+  }
+
+  group('AssetBundle asset variants', () {
+    FileSystem testFileSystem;
+    setUp(() async {
+      testFileSystem = MemoryFileSystem(
+        style: platform.isWindows
+          ? FileSystemStyle.windows
+          : FileSystemStyle.posix,
+      );
+      testFileSystem.currentDirectory = testFileSystem.systemTempDirectory.createTempSync('flutter_asset_bundle_variant_test.');
+    });
+
+    testUsingContext('main asset and variants', () async {
+      // Setting flutterRoot here so that it picks up the MemoryFileSystem's
+      // path separator.
+      Cache.flutterRoot = getFlutterRoot();
+      writeEmptySchemaFile(fs);
+
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(
+'''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  assets:
+    - a/b/c/foo
+'''
+      );
+      fs.file('.packages')..createSync();
+
+      final List<String> assets = <String>[
+        'a/b/c/foo',
+        'a/b/c/var1/foo',
+        'a/b/c/var2/foo',
+        'a/b/c/var3/foo',
+      ];
+      for (String asset in assets) {
+        fs.file(fixPath(asset))
+          ..createSync(recursive: true)
+          ..writeAsStringSync(asset);
+      }
+
+      AssetBundle bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+
+      // The main asset file, /a/b/c/foo, and its variants exist.
+      for (String asset in assets) {
+        expect(bundle.entries.containsKey(asset), true);
+        expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset);
+      }
+
+      fs.file(fixPath('a/b/c/foo')).deleteSync();
+      bundle = AssetBundleFactory.instance.createBundle();
+      await bundle.build(manifestPath: 'pubspec.yaml');
+
+      // Now the main asset file, /a/b/c/foo, does not exist. This is OK because
+      // the /a/b/c/*/foo variants do exist.
+      expect(bundle.entries.containsKey('a/b/c/foo'), false);
+      for (String asset in assets.skip(1)) {
+        expect(bundle.entries.containsKey(asset), true);
+        expect(utf8.decode(await bundle.entries[asset].contentsAsBytes()), asset);
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/asset_test.dart b/packages/flutter_tools/test/general.shard/asset_test.dart
new file mode 100644
index 0000000..65782de
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/asset_test.dart
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/asset.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('Assets', () {
+    final String dataPath = fs.path.join(
+      getFlutterRoot(),
+      'packages',
+      'flutter_tools',
+      'test',
+      'data',
+      'asset_test',
+    );
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    // This test intentionally does not use a memory file system to ensure
+    // that AssetBundle with fonts also works on Windows.
+    testUsingContext('app font uses local font file', () async {
+      final AssetBundle asset = AssetBundleFactory.instance.createBundle();
+      await asset.build(
+        manifestPath : fs.path.join(dataPath, 'main', 'pubspec.yaml'),
+        packagesPath: fs.path.join(dataPath, 'main', '.packages'),
+        includeDefaultFonts: false,
+      );
+
+      expect(asset.entries.containsKey('FontManifest.json'), isTrue);
+      expect(
+        await getValueAsString('FontManifest.json', asset),
+        '[{"family":"packages/font/test_font","fonts":[{"asset":"packages/font/test_font_file"}]}]',
+      );
+      expect(asset.wasBuiltOnce(), true);
+    });
+
+    testUsingContext('handles empty pubspec with .packages', () async {
+      final String dataPath = fs.path.join(
+        getFlutterRoot(),
+        'packages',
+        'flutter_tools',
+        'test',
+        'data',
+        'fuchsia_test',
+      );
+      final AssetBundle asset = AssetBundleFactory.instance.createBundle();
+      await asset.build(
+        manifestPath : fs.path.join(dataPath, 'main', 'pubspec.yaml'), // file doesn't exist
+        packagesPath: fs.path.join(dataPath, 'main', '.packages'),
+        includeDefaultFonts: false,
+      );
+      expect(asset.wasBuiltOnce(), true);
+    });
+  });
+}
+
+Future<String> getValueAsString(String key, AssetBundle asset) async {
+  return String.fromCharCodes(await asset.entries[key].contentsAsBytes());
+}
diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart
new file mode 100644
index 0000000..13f96b9
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/build_test.dart
@@ -0,0 +1,691 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/android/android_sdk.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/base/build.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/process.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockFlutterVersion extends Mock implements FlutterVersion {}
+class MockAndroidSdk extends Mock implements AndroidSdk {}
+class MockArtifacts extends Mock implements Artifacts {}
+class MockXcode extends Mock implements Xcode {}
+
+class _FakeGenSnapshot implements GenSnapshot {
+  _FakeGenSnapshot({
+    this.succeed = true,
+  });
+
+  final bool succeed;
+  Map<String, String> outputs = <String, String>{};
+  int _callCount = 0;
+  SnapshotType _snapshotType;
+  String _depfilePath;
+  List<String> _additionalArgs;
+
+  int get callCount => _callCount;
+
+  SnapshotType get snapshotType => _snapshotType;
+
+  String get depfilePath => _depfilePath;
+
+  List<String> get additionalArgs => _additionalArgs;
+
+  @override
+  Future<int> run({
+    SnapshotType snapshotType,
+    String depfilePath,
+    IOSArch iosArch,
+    Iterable<String> additionalArgs = const <String>[],
+  }) async {
+    _callCount += 1;
+    _snapshotType = snapshotType;
+    _depfilePath = depfilePath;
+    _additionalArgs = additionalArgs.toList();
+
+    if (!succeed)
+      return 1;
+    outputs.forEach((String filePath, String fileContent) {
+      fs.file(filePath).writeAsString(fileContent);
+    });
+    return 0;
+  }
+}
+
+void main() {
+  group('SnapshotType', () {
+    test('throws, if build mode is null', () {
+      expect(
+        () => SnapshotType(TargetPlatform.android_x64, null),
+        throwsA(anything),
+      );
+    });
+    test('does not throw, if target platform is null', () {
+      expect(SnapshotType(null, BuildMode.release), isNotNull);
+    });
+  });
+
+  group('Snapshotter - AOT', () {
+    const String kSnapshotDart = 'snapshot.dart';
+    String skyEnginePath;
+
+    _FakeGenSnapshot genSnapshot;
+    MemoryFileSystem fs;
+    AOTSnapshotter snapshotter;
+    AOTSnapshotter snapshotterWithTimings;
+    MockAndroidSdk mockAndroidSdk;
+    MockArtifacts mockArtifacts;
+    MockXcode mockXcode;
+    BufferLogger bufferLogger;
+
+    setUp(() async {
+      fs = MemoryFileSystem();
+      fs.file(kSnapshotDart).createSync();
+      fs.file('.packages').writeAsStringSync('sky_engine:file:///flutter/bin/cache/pkg/sky_engine/lib/');
+
+      skyEnginePath = fs.path.fromUri(Uri.file('/flutter/bin/cache/pkg/sky_engine'));
+      fs.directory(fs.path.join(skyEnginePath, 'lib', 'ui')).createSync(recursive: true);
+      fs.directory(fs.path.join(skyEnginePath, 'sdk_ext')).createSync(recursive: true);
+      fs.file(fs.path.join(skyEnginePath, '.packages')).createSync();
+      fs.file(fs.path.join(skyEnginePath, 'lib', 'ui', 'ui.dart')).createSync();
+      fs.file(fs.path.join(skyEnginePath, 'sdk_ext', 'vmservice_io.dart')).createSync();
+
+      genSnapshot = _FakeGenSnapshot();
+      snapshotter = AOTSnapshotter();
+      snapshotterWithTimings = AOTSnapshotter(reportTimings: true);
+      mockAndroidSdk = MockAndroidSdk();
+      mockArtifacts = MockArtifacts();
+      mockXcode = MockXcode();
+      bufferLogger = BufferLogger();
+      for (BuildMode mode in BuildMode.values) {
+        when(mockArtifacts.getArtifactPath(Artifact.snapshotDart,
+            platform: anyNamed('platform'), mode: mode)).thenReturn(kSnapshotDart);
+      }
+    });
+
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Artifacts: () => mockArtifacts,
+      FileSystem: () => fs,
+      GenSnapshot: () => genSnapshot,
+      Xcode: () => mockXcode,
+      Logger: () => bufferLogger,
+    };
+
+    testUsingContext('iOS debug AOT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      ), isNot(equals(0)));
+    }, overrides: contextOverrides);
+
+    testUsingContext('Android arm debug AOT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      ), isNot(0));
+    }, overrides: contextOverrides);
+
+    testUsingContext('Android arm64 debug AOT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.android_arm64,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      ), isNot(0));
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS armv7 profile AOT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(xcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        iosArch: IOSArch.armv7,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS arm64 profile AOT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(xcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        iosArch: IOSArch.arm64,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS release armv7 AOT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(xcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        iosArch: IOSArch.armv7,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds iOS release arm64 AOT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'snapshot_assembly.S'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(xcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        iosArch: IOSArch.arm64,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.ios);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-assembly',
+        '--assembly=${fs.path.join(outputPath, 'snapshot_assembly.S')}',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds shared library for android-arm64', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm64,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-aot-elf',
+        '--elf=build/foo/app.so',
+        '--strip',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('reports timing', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'app.so'): '',
+      };
+
+      final RunResult successResult = RunResult(ProcessResult(1, 0, '', ''), <String>['command name', 'arguments...']);
+      when(xcode.cc(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+      when(xcode.clang(any)).thenAnswer((_) => Future<RunResult>.value(successResult));
+
+      final int genSnapshotExitCode = await snapshotterWithTimings.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(bufferLogger.statusText, matches(RegExp(r'snapshot\(CompileTime\): \d+ ms.')));
+    }, overrides: contextOverrides);
+  });
+
+  group('Snapshotter - JIT', () {
+    const String kTrace = 'trace.txt';
+    const String kEngineVmSnapshotData = 'engine_vm_snapshot_data';
+    const String kEngineIsolateSnapshotData = 'engine_isolate_snapshot_data';
+
+    _FakeGenSnapshot genSnapshot;
+    MemoryFileSystem fs;
+    JITSnapshotter snapshotter;
+    MockAndroidSdk mockAndroidSdk;
+    MockArtifacts mockArtifacts;
+
+    setUp(() async {
+      fs = MemoryFileSystem();
+      fs.file(kTrace).createSync();
+      fs.file(kEngineVmSnapshotData).createSync();
+      fs.file(kEngineIsolateSnapshotData).createSync();
+
+      genSnapshot = _FakeGenSnapshot();
+      snapshotter = JITSnapshotter();
+      mockAndroidSdk = MockAndroidSdk();
+      mockArtifacts = MockArtifacts();
+
+      for (BuildMode mode in BuildMode.values) {
+        when(mockArtifacts.getArtifactPath(Artifact.vmSnapshotData,
+            platform: anyNamed('platform'), mode: mode))
+            .thenReturn(kEngineVmSnapshotData);
+        when(mockArtifacts.getArtifactPath(Artifact.isolateSnapshotData,
+            platform: anyNamed('platform'), mode: mode))
+            .thenReturn(kEngineIsolateSnapshotData);
+      }
+    });
+
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Artifacts: () => mockArtifacts,
+      FileSystem: () => fs,
+      GenSnapshot: () => genSnapshot,
+    };
+
+    testUsingContext('iOS debug JIT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      ), isNot(equals(0)));
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm debug JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.debug);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--enable_asserts',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm64 debug JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm64,
+        buildMode: BuildMode.debug,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
+      expect(genSnapshot.snapshotType.mode, BuildMode.debug);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--enable_asserts',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('iOS release JIT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      ), isNot(equals(0)));
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm profile JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm64 profile JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm64,
+        buildMode: BuildMode.profile,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
+      expect(genSnapshot.snapshotType.mode, BuildMode.profile);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('iOS release JIT snapshot is invalid', () async {
+      final String outputPath = fs.path.join('build', 'foo');
+      expect(await snapshotter.build(
+        platform: TargetPlatform.ios,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      ), isNot(equals(0)));
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm release JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        '--no-sim-use-hardfp',
+        '--no-use-integer-division',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+    testUsingContext('builds Android arm64 release JIT snapshot', () async {
+      fs.file('main.dill').writeAsStringSync('binary magic');
+
+      final String outputPath = fs.path.join('build', 'foo');
+      fs.directory(outputPath).createSync(recursive: true);
+
+      genSnapshot.outputs = <String, String>{
+        fs.path.join(outputPath, 'isolate_snapshot_data'): '',
+        fs.path.join(outputPath, 'isolate_snapshot_instr'): '',
+      };
+
+      final int genSnapshotExitCode = await snapshotter.build(
+        platform: TargetPlatform.android_arm64,
+        buildMode: BuildMode.release,
+        mainPath: 'main.dill',
+        packagesPath: '.packages',
+        outputPath: outputPath,
+        compilationTraceFilePath: kTrace,
+      );
+
+      expect(genSnapshotExitCode, 0);
+      expect(genSnapshot.callCount, 1);
+      expect(genSnapshot.snapshotType.platform, TargetPlatform.android_arm64);
+      expect(genSnapshot.snapshotType.mode, BuildMode.release);
+      expect(genSnapshot.additionalArgs, <String>[
+        '--deterministic',
+        '--snapshot_kind=app-jit',
+        '--load_compilation_trace=$kTrace',
+        '--load_vm_snapshot_data=$kEngineVmSnapshotData',
+        '--load_isolate_snapshot_data=$kEngineIsolateSnapshotData',
+        '--isolate_snapshot_data=build/foo/isolate_snapshot_data',
+        '--isolate_snapshot_instructions=build/foo/isolate_snapshot_instr',
+        'main.dill',
+      ]);
+    }, overrides: contextOverrides);
+
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/common_test.dart b/packages/flutter_tools/test/general.shard/base/common_test.dart
new file mode 100644
index 0000000..76f9fd7
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/common_test.dart
@@ -0,0 +1,27 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/common.dart';
+
+import '../../src/common.dart';
+
+void main() {
+  group('throwToolExit', () {
+    test('throws ToolExit', () {
+      expect(() => throwToolExit('message'), throwsToolExit());
+    });
+
+    test('throws ToolExit with exitCode', () {
+      expect(() => throwToolExit('message', exitCode: 42), throwsToolExit(exitCode: 42));
+    });
+
+    test('throws ToolExit with message', () {
+      expect(() => throwToolExit('message'), throwsToolExit(message: 'message'));
+    });
+
+    test('throws ToolExit with message and exit code', () {
+      expect(() => throwToolExit('message', exitCode: 42), throwsToolExit(exitCode: 42, message: 'message'));
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/context_test.dart b/packages/flutter_tools/test/general.shard/base/context_test.dart
new file mode 100644
index 0000000..c8b46ff
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/context_test.dart
@@ -0,0 +1,277 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/context.dart';
+
+import '../../src/common.dart';
+
+void main() {
+  group('AppContext', () {
+    group('global getter', () {
+      bool called;
+
+      setUp(() {
+        called = false;
+      });
+
+      test('returns non-null context in the root zone', () {
+        expect(context, isNotNull);
+      });
+
+      test('returns root context in child of root zone if zone was manually created', () {
+        final Zone rootZone = Zone.current;
+        final AppContext rootContext = context;
+        runZoned<void>(() {
+          expect(Zone.current, isNot(rootZone));
+          expect(Zone.current.parent, rootZone);
+          expect(context, rootContext);
+          called = true;
+        });
+        expect(called, isTrue);
+      });
+
+      test('returns child context after run', () async {
+        final AppContext rootContext = context;
+        await rootContext.run<void>(name: 'child', body: () {
+          expect(context, isNot(rootContext));
+          expect(context.name, 'child');
+          called = true;
+        });
+        expect(called, isTrue);
+      });
+
+      test('returns grandchild context after nested run', () async {
+        final AppContext rootContext = context;
+        await rootContext.run<void>(name: 'child', body: () async {
+          final AppContext childContext = context;
+          await childContext.run<void>(name: 'grandchild', body: () {
+            expect(context, isNot(rootContext));
+            expect(context, isNot(childContext));
+            expect(context.name, 'grandchild');
+            called = true;
+          });
+        });
+        expect(called, isTrue);
+      });
+
+      test('scans up zone hierarchy for first context', () async {
+        final AppContext rootContext = context;
+        await rootContext.run<void>(name: 'child', body: () {
+          final AppContext childContext = context;
+          runZoned<void>(() {
+            expect(context, isNot(rootContext));
+            expect(context, same(childContext));
+            expect(context.name, 'child');
+            called = true;
+          });
+        });
+        expect(called, isTrue);
+      });
+    });
+
+    group('operator[]', () {
+      test('still finds values if async code runs after body has finished', () async {
+        final Completer<void> outer = Completer<void>();
+        final Completer<void> inner = Completer<void>();
+        String value;
+        await context.run<void>(
+          body: () {
+            outer.future.then<void>((_) {
+              value = context.get<String>();
+              inner.complete();
+            });
+          },
+          fallbacks: <Type, Generator>{
+            String: () => 'value',
+          },
+        );
+        expect(value, isNull);
+        outer.complete();
+        await inner.future;
+        expect(value, 'value');
+      });
+
+      test('caches generated override values', () async {
+        int consultationCount = 0;
+        String value;
+        await context.run<void>(
+          body: () async {
+            final StringBuffer buf = StringBuffer(context.get<String>());
+            buf.write(context.get<String>());
+            await context.run<void>(body: () {
+              buf.write(context.get<String>());
+            });
+            value = buf.toString();
+          },
+          overrides: <Type, Generator>{
+            String: () {
+              consultationCount++;
+              return 'v';
+            },
+          },
+        );
+        expect(value, 'vvv');
+        expect(consultationCount, 1);
+      });
+
+      test('caches generated fallback values', () async {
+        int consultationCount = 0;
+        String value;
+        await context.run(
+          body: () async {
+            final StringBuffer buf = StringBuffer(context.get<String>());
+            buf.write(context.get<String>());
+            await context.run<void>(body: () {
+              buf.write(context.get<String>());
+            });
+            value = buf.toString();
+          },
+          fallbacks: <Type, Generator>{
+            String: () {
+              consultationCount++;
+              return 'v';
+            },
+          },
+        );
+        expect(value, 'vvv');
+        expect(consultationCount, 1);
+      });
+
+      test('returns null if generated value is null', () async {
+        final String value = await context.run<String>(
+          body: () => context.get<String>(),
+          overrides: <Type, Generator>{
+            String: () => null,
+          },
+        );
+        expect(value, isNull);
+      });
+
+      test('throws if generator has dependency cycle', () async {
+        final Future<String> value = context.run<String>(
+          body: () async {
+            return context.get<String>();
+          },
+          fallbacks: <Type, Generator>{
+            int: () => int.parse(context.get<String>()),
+            String: () => '${context.get<double>()}',
+            double: () => context.get<int>() * 1.0,
+          },
+        );
+        try {
+          await value;
+          fail('ContextDependencyCycleException expected but not thrown.');
+        } on ContextDependencyCycleException catch (e) {
+          expect(e.cycle, <Type>[String, double, int]);
+          expect(e.toString(), 'Dependency cycle detected: String -> double -> int');
+        }
+      });
+    });
+
+    group('run', () {
+      test('returns the value returned by body', () async {
+        expect(await context.run<int>(body: () => 123), 123);
+        expect(await context.run<String>(body: () => 'value'), 'value');
+        expect(await context.run<int>(body: () async => 456), 456);
+      });
+
+      test('passes name to child context', () async {
+        await context.run<void>(name: 'child', body: () {
+          expect(context.name, 'child');
+        });
+      });
+
+      group('fallbacks', () {
+        bool called;
+
+        setUp(() {
+          called = false;
+        });
+
+        test('are applied after parent context is consulted', () async {
+          final String value = await context.run<String>(
+            body: () {
+              return context.run<String>(
+                body: () {
+                  called = true;
+                  return context.get<String>();
+                },
+                fallbacks: <Type, Generator>{
+                  String: () => 'child',
+                },
+              );
+            },
+          );
+          expect(called, isTrue);
+          expect(value, 'child');
+        });
+
+        test('are not applied if parent context supplies value', () async {
+          bool childConsulted = false;
+          final String value = await context.run<String>(
+            body: () {
+              return context.run<String>(
+                body: () {
+                  called = true;
+                  return context.get<String>();
+                },
+                fallbacks: <Type, Generator>{
+                  String: () {
+                    childConsulted = true;
+                    return 'child';
+                  },
+                },
+              );
+            },
+            fallbacks: <Type, Generator>{
+              String: () => 'parent',
+            },
+          );
+          expect(called, isTrue);
+          expect(value, 'parent');
+          expect(childConsulted, isFalse);
+        });
+
+        test('may depend on one another', () async {
+          final String value = await context.run<String>(
+            body: () {
+              return context.get<String>();
+            },
+            fallbacks: <Type, Generator>{
+              int: () => 123,
+              String: () => '-${context.get<int>()}-',
+            },
+          );
+          expect(value, '-123-');
+        });
+      });
+
+      group('overrides', () {
+        test('intercept consultation of parent context', () async {
+          bool parentConsulted = false;
+          final String value = await context.run<String>(
+            body: () {
+              return context.run<String>(
+                body: () => context.get<String>(),
+                overrides: <Type, Generator>{
+                  String: () => 'child',
+                },
+              );
+            },
+            fallbacks: <Type, Generator>{
+              String: () {
+                parentConsulted = true;
+                return 'parent';
+              },
+            },
+          );
+          expect(value, 'child');
+          expect(parentConsulted, isFalse);
+        });
+      });
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/file_system_test.dart b/packages/flutter_tools/test/general.shard/base/file_system_test.dart
new file mode 100644
index 0000000..09126ac
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/file_system_test.dart
@@ -0,0 +1,106 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:platform/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('ensureDirectoryExists', () {
+    MemoryFileSystem fs;
+
+    setUp(() {
+      fs = MemoryFileSystem();
+    });
+
+    testUsingContext('recursively creates a directory if it does not exist', () async {
+      ensureDirectoryExists('foo/bar/baz.flx');
+      expect(fs.isDirectorySync('foo/bar'), true);
+    }, overrides: <Type, Generator>{FileSystem: () => fs});
+
+    testUsingContext('throws tool exit on failure to create', () async {
+      fs.file('foo').createSync();
+      expect(() => ensureDirectoryExists('foo/bar.flx'), throwsToolExit());
+    }, overrides: <Type, Generator>{FileSystem: () => fs});
+  });
+
+  group('copyDirectorySync', () {
+    /// Test file_systems.copyDirectorySync() using MemoryFileSystem.
+    /// Copies between 2 instances of file systems which is also supported by copyDirectorySync().
+    test('test directory copy', () async {
+      final MemoryFileSystem sourceMemoryFs = MemoryFileSystem();
+      const String sourcePath = '/some/origin';
+      final Directory sourceDirectory = await sourceMemoryFs.directory(sourcePath).create(recursive: true);
+      sourceMemoryFs.currentDirectory = sourcePath;
+      final File sourceFile1 = sourceMemoryFs.file('some_file.txt')..writeAsStringSync('bleh');
+      final DateTime writeTime = sourceFile1.lastModifiedSync();
+      sourceMemoryFs.file('sub_dir/another_file.txt').createSync(recursive: true);
+      sourceMemoryFs.directory('empty_directory').createSync();
+
+      // Copy to another memory file system instance.
+      final MemoryFileSystem targetMemoryFs = MemoryFileSystem();
+      const String targetPath = '/some/non-existent/target';
+      final Directory targetDirectory = targetMemoryFs.directory(targetPath);
+      copyDirectorySync(sourceDirectory, targetDirectory);
+
+      expect(targetDirectory.existsSync(), true);
+      targetMemoryFs.currentDirectory = targetPath;
+      expect(targetMemoryFs.directory('empty_directory').existsSync(), true);
+      expect(targetMemoryFs.file('sub_dir/another_file.txt').existsSync(), true);
+      expect(targetMemoryFs.file('some_file.txt').readAsStringSync(), 'bleh');
+
+      // Assert that the copy operation hasn't modified the original file in some way.
+      expect(sourceMemoryFs.file('some_file.txt').lastModifiedSync(), writeTime);
+      // There's still 3 things in the original directory as there were initially.
+      expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3);
+    });
+  });
+
+  group('canonicalizePath', () {
+    test('does not lowercase on Windows', () {
+      String path = 'C:\\Foo\\bAr\\cOOL.dart';
+      expect(canonicalizePath(path), path);
+      // fs.path.canonicalize does lowercase on Windows
+      expect(fs.path.canonicalize(path), isNot(path));
+
+      path = '..\\bar\\.\\\\Foo';
+      final String expected = fs.path.join(fs.currentDirectory.parent.absolute.path, 'bar', 'Foo');
+      expect(canonicalizePath(path), expected);
+      // fs.path.canonicalize should return the same result (modulo casing)
+      expect(fs.path.canonicalize(path), expected.toLowerCase());
+    }, testOn: 'windows');
+
+    test('does not lowercase on posix', () {
+      String path = '/Foo/bAr/cOOL.dart';
+      expect(canonicalizePath(path), path);
+      // fs.path.canonicalize and canonicalizePath should be the same on Posix
+      expect(fs.path.canonicalize(path), path);
+
+      path = '../bar/.//Foo';
+      final String expected = fs.path.join(fs.currentDirectory.parent.absolute.path, 'bar', 'Foo');
+      expect(canonicalizePath(path), expected);
+    }, testOn: 'posix');
+  });
+
+  group('escapePath', () {
+    testUsingContext('on Windows', () {
+      expect(escapePath('C:\\foo\\bar\\cool.dart'), 'C:\\\\foo\\\\bar\\\\cool.dart');
+      expect(escapePath('foo\\bar\\cool.dart'), 'foo\\\\bar\\\\cool.dart');
+      expect(escapePath('C:/foo/bar/cool.dart'), 'C:/foo/bar/cool.dart');
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(operatingSystem: 'windows'),
+    });
+
+    testUsingContext('on Linux', () {
+      expect(escapePath('/foo/bar/cool.dart'), '/foo/bar/cool.dart');
+      expect(escapePath('foo/bar/cool.dart'), 'foo/bar/cool.dart');
+      expect(escapePath('foo\\cool.dart'), 'foo\\cool.dart');
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/fingerprint_test.dart b/packages/flutter_tools/test/general.shard/base/fingerprint_test.dart
new file mode 100644
index 0000000..9c1f5a8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/fingerprint_test.dart
@@ -0,0 +1,521 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show json;
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/fingerprint.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('Fingerprinter', () {
+    const String kVersion = '123456abcdef';
+
+    MemoryFileSystem fs;
+    MockFlutterVersion mockVersion;
+
+    setUp(() {
+      fs = MemoryFileSystem();
+      mockVersion = MockFlutterVersion();
+      when(mockVersion.frameworkRevision).thenReturn(kVersion);
+    });
+
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{
+      FileSystem: () => fs,
+    };
+
+    testUsingContext('throws when depfile is malformed', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+      await fs.file('depfile').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      expect(() async => await fingerprinter.buildFingerprint(), throwsA(anything));
+    }, overrides: contextOverrides);
+
+    testUsingContext('creates fingerprint with specified properties and files', () async {
+      await fs.file('a.dart').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart'],
+        properties: <String, String>{
+          'foo': 'bar',
+          'wibble': 'wobble',
+        },
+      );
+      final Fingerprint fingerprint = await fingerprinter.buildFingerprint();
+      expect(fingerprint, Fingerprint.fromBuildInputs(<String, String>{
+        'foo': 'bar',
+        'wibble': 'wobble',
+      }, <String>['a.dart']));
+    }, overrides: contextOverrides);
+
+    testUsingContext('creates fingerprint with file checksums', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+      await fs.file('depfile').writeAsString('depfile : b.dart');
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      final Fingerprint fingerprint = await fingerprinter.buildFingerprint();
+      expect(fingerprint, Fingerprint.fromBuildInputs(<String, String>{
+        'bar': 'baz',
+        'wobble': 'womble',
+      }, <String>['a.dart', 'b.dart']));
+    }, overrides: contextOverrides);
+
+    testUsingContext('fingerprint does not match if not present', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      expect(await fingerprinter.doesFingerprintMatch(), isFalse);
+    }, overrides: contextOverrides);
+
+    testUsingContext('fingerprint does match if different', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+
+      final Fingerprinter fingerprinter1 = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      await fingerprinter1.writeFingerprint();
+
+      final Fingerprinter fingerprinter2 = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'elbmow',
+        },
+      );
+      expect(await fingerprinter2.doesFingerprintMatch(), isFalse);
+    }, overrides: contextOverrides);
+
+    testUsingContext('fingerprint does not match if depfile is malformed', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+      await fs.file('depfile').writeAsString('depfile : b.dart');
+
+      // Write a valid fingerprint
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      await fingerprinter.writeFingerprint();
+
+      // Write a corrupt depfile.
+      await fs.file('depfile').writeAsString('');
+      final Fingerprinter badFingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+
+      expect(await badFingerprinter.doesFingerprintMatch(), isFalse);
+    }, overrides: contextOverrides);
+
+    testUsingContext('fingerprint does not match if previous fingerprint is malformed', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+      await fs.file('out.fingerprint').writeAsString('** not JSON **');
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      expect(await fingerprinter.doesFingerprintMatch(), isFalse);
+    }, overrides: contextOverrides);
+
+    testUsingContext('fingerprint does match if identical', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      await fingerprinter.writeFingerprint();
+      expect(await fingerprinter.doesFingerprintMatch(), isTrue);
+    }, overrides: contextOverrides);
+
+    final Platform mockPlatformDisabledCache = MockPlatform();
+    mockPlatformDisabledCache.environment['DISABLE_FLUTTER_BUILD_CACHE']  = 'true';
+    testUsingContext('can be disabled with an environment variable', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      await fingerprinter.writeFingerprint();
+      expect(await fingerprinter.doesFingerprintMatch(), isFalse);
+    }, overrides: <Type, Generator>{
+      Platform: () => mockPlatformDisabledCache,
+      ...contextOverrides,
+    });
+
+    final Platform mockPlatformEnabledCache = MockPlatform();
+    mockPlatformEnabledCache.environment['DISABLE_FLUTTER_BUILD_CACHE']  = 'false';
+    testUsingContext('can be not-disabled with an environment variable', () async {
+      await fs.file('a.dart').create();
+      await fs.file('b.dart').create();
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart', 'b.dart'],
+        properties: <String, String>{
+          'bar': 'baz',
+          'wobble': 'womble',
+        },
+      );
+      await fingerprinter.writeFingerprint();
+      expect(await fingerprinter.doesFingerprintMatch(), isTrue);
+    }, overrides: <Type, Generator>{
+      Platform: () => mockPlatformEnabledCache,
+      ...contextOverrides,
+    });
+
+    testUsingContext('fails to write fingerprint if inputs are missing', () async {
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart'],
+        properties: <String, String>{
+          'foo': 'bar',
+          'wibble': 'wobble',
+        },
+      );
+      await fingerprinter.writeFingerprint();
+      expect(fs.file('out.fingerprint').existsSync(), isFalse);
+    }, overrides: contextOverrides);
+
+    testUsingContext('applies path filter to inputs paths', () async {
+      await fs.file('a.dart').create();
+      await fs.file('ab.dart').create();
+      await fs.file('depfile').writeAsString('depfile : ab.dart c.dart');
+
+      final Fingerprinter fingerprinter = Fingerprinter(
+        fingerprintPath: 'out.fingerprint',
+        paths: <String>['a.dart'],
+        depfilePaths: <String>['depfile'],
+        properties: <String, String>{
+          'foo': 'bar',
+          'wibble': 'wobble',
+        },
+        pathFilter: (String path) => path.startsWith('a'),
+      );
+      await fingerprinter.writeFingerprint();
+      expect(fs.file('out.fingerprint').existsSync(), isTrue);
+    }, overrides: contextOverrides);
+  });
+
+  group('Fingerprint', () {
+    MockFlutterVersion mockVersion;
+    const String kVersion = '123456abcdef';
+
+    setUp(() {
+      mockVersion = MockFlutterVersion();
+      when(mockVersion.frameworkRevision).thenReturn(kVersion);
+    });
+
+    group('fromBuildInputs', () {
+      MemoryFileSystem fs;
+
+      setUp(() {
+        fs = MemoryFileSystem();
+      });
+
+      testUsingContext('throws if any input file does not exist', () async {
+        await fs.file('a.dart').create();
+        expect(
+          () => Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']),
+          throwsArgumentError,
+        );
+      }, overrides: <Type, Generator>{FileSystem: () => fs});
+
+      testUsingContext('populates checksums for valid files', () async {
+        await fs.file('a.dart').writeAsString('This is a');
+        await fs.file('b.dart').writeAsString('This is b');
+        final Fingerprint fingerprint = Fingerprint.fromBuildInputs(<String, String>{}, <String>['a.dart', 'b.dart']);
+
+        final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
+        expect(jsonObject['files'], hasLength(2));
+        expect(jsonObject['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
+        expect(jsonObject['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
+      }, overrides: <Type, Generator>{FileSystem: () => fs});
+
+      testUsingContext('includes framework version', () {
+        final Fingerprint fingerprint = Fingerprint.fromBuildInputs(<String, String>{}, <String>[]);
+
+        final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
+        expect(jsonObject['version'], mockVersion.frameworkRevision);
+      }, overrides: <Type, Generator>{FlutterVersion: () => mockVersion});
+
+      testUsingContext('includes provided properties', () {
+        final Fingerprint fingerprint = Fingerprint.fromBuildInputs(<String, String>{'a': 'A', 'b': 'B'}, <String>[]);
+
+        final Map<String, dynamic> jsonObject = json.decode(fingerprint.toJson());
+        expect(jsonObject['properties'], hasLength(2));
+        expect(jsonObject['properties']['a'], 'A');
+        expect(jsonObject['properties']['b'], 'B');
+      }, overrides: <Type, Generator>{FlutterVersion: () => mockVersion});
+    });
+
+    group('fromJson', () {
+      testUsingContext('throws if JSON is invalid', () async {
+        expect(() => Fingerprint.fromJson('<xml></xml>'), throwsA(anything));
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('creates fingerprint from valid JSON', () async {
+        final String jsonString = json.encode(<String, dynamic>{
+          'version': kVersion,
+          'properties': <String, String>{
+            'buildMode': BuildMode.release.toString(),
+            'targetPlatform': TargetPlatform.ios.toString(),
+            'entryPoint': 'a.dart',
+          },
+          'files': <String, dynamic>{
+            'a.dart': '8a21a15fad560b799f6731d436c1b698',
+            'b.dart': '6f144e08b58cd0925328610fad7ac07c',
+          },
+        });
+        final Fingerprint fingerprint = Fingerprint.fromJson(jsonString);
+        final Map<String, dynamic> content = json.decode(fingerprint.toJson());
+        expect(content, hasLength(3));
+        expect(content['version'], mockVersion.frameworkRevision);
+        expect(content['properties'], hasLength(3));
+        expect(content['properties']['buildMode'], BuildMode.release.toString());
+        expect(content['properties']['targetPlatform'], TargetPlatform.ios.toString());
+        expect(content['properties']['entryPoint'], 'a.dart');
+        expect(content['files'], hasLength(2));
+        expect(content['files']['a.dart'], '8a21a15fad560b799f6731d436c1b698');
+        expect(content['files']['b.dart'], '6f144e08b58cd0925328610fad7ac07c');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('throws ArgumentError for unknown versions', () async {
+        final String jsonString = json.encode(<String, dynamic>{
+          'version': 'bad',
+          'properties': <String, String>{},
+          'files': <String, String>{},
+        });
+        expect(() => Fingerprint.fromJson(jsonString), throwsArgumentError);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('throws ArgumentError if version is not present', () async {
+        final String jsonString = json.encode(<String, dynamic>{
+          'properties': <String, String>{},
+          'files': <String, String>{},
+        });
+        expect(() => Fingerprint.fromJson(jsonString), throwsArgumentError);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('treats missing properties and files entries as if empty', () async {
+        final String jsonString = json.encode(<String, dynamic>{
+          'version': kVersion,
+        });
+        expect(Fingerprint.fromJson(jsonString), Fingerprint.fromBuildInputs(<String, String>{}, <String>[]));
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+    });
+
+    group('operator ==', () {
+      testUsingContext('reports not equal if properties do not match', () async {
+        final Map<String, dynamic> a = <String, dynamic>{
+          'version': kVersion,
+          'properties': <String, String>{
+            'buildMode': BuildMode.debug.toString(),
+          },
+          'files': <String, dynamic>{},
+        };
+        final Map<String, dynamic> b = Map<String, dynamic>.from(a);
+        b['properties'] = <String, String>{
+          'buildMode': BuildMode.release.toString(),
+        };
+        expect(Fingerprint.fromJson(json.encode(a)) == Fingerprint.fromJson(json.encode(b)), isFalse);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('reports not equal if file checksums do not match', () async {
+        final Map<String, dynamic> a = <String, dynamic>{
+          'version': kVersion,
+          'properties': <String, String>{},
+          'files': <String, dynamic>{
+            'a.dart': '8a21a15fad560b799f6731d436c1b698',
+            'b.dart': '6f144e08b58cd0925328610fad7ac07c',
+          },
+        };
+        final Map<String, dynamic> b = Map<String, dynamic>.from(a);
+        b['files'] = <String, dynamic>{
+          'a.dart': '8a21a15fad560b799f6731d436c1b698',
+          'b.dart': '6f144e08b58cd0925328610fad7ac07d',
+        };
+        expect(Fingerprint.fromJson(json.encode(a)) == Fingerprint.fromJson(json.encode(b)), isFalse);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('reports not equal if file paths do not match', () async {
+        final Map<String, dynamic> a = <String, dynamic>{
+          'version': kVersion,
+          'properties': <String, String>{},
+          'files': <String, dynamic>{
+            'a.dart': '8a21a15fad560b799f6731d436c1b698',
+            'b.dart': '6f144e08b58cd0925328610fad7ac07c',
+          },
+        };
+        final Map<String, dynamic> b = Map<String, dynamic>.from(a);
+        b['files'] = <String, dynamic>{
+          'a.dart': '8a21a15fad560b799f6731d436c1b698',
+          'c.dart': '6f144e08b58cd0925328610fad7ac07d',
+        };
+        expect(Fingerprint.fromJson(json.encode(a)) == Fingerprint.fromJson(json.encode(b)), isFalse);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+      testUsingContext('reports equal if properties and file checksums match', () async {
+        final Map<String, dynamic> a = <String, dynamic>{
+          'version': kVersion,
+          'properties': <String, String>{
+            'buildMode': BuildMode.debug.toString(),
+            'targetPlatform': TargetPlatform.ios.toString(),
+            'entryPoint': 'a.dart',
+          },
+          'files': <String, dynamic>{
+            'a.dart': '8a21a15fad560b799f6731d436c1b698',
+            'b.dart': '6f144e08b58cd0925328610fad7ac07c',
+          },
+        };
+        expect(Fingerprint.fromJson(json.encode(a)) == Fingerprint.fromJson(json.encode(a)), isTrue);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+    });
+    group('hashCode', () {
+      testUsingContext('is consistent with equals, even if map entries are reordered', () async {
+        final Fingerprint a = Fingerprint.fromJson('{"version":"$kVersion","properties":{"a":"A","b":"B"},"files":{}}');
+        final Fingerprint b = Fingerprint.fromJson('{"version":"$kVersion","properties":{"b":"B","a":"A"},"files":{}}');
+        expect(a, b);
+        expect(a.hashCode, b.hashCode);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => mockVersion,
+      });
+
+    });
+  });
+
+  group('readDepfile', () {
+    MemoryFileSystem fs;
+
+    setUp(() {
+      fs = MemoryFileSystem();
+    });
+
+    final Map<Type, Generator> contextOverrides = <Type, Generator>{FileSystem: () => fs};
+
+    testUsingContext('returns one file if only one is listed', () async {
+      await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart');
+      expect(await readDepfile('a.d'), unorderedEquals(<String>['/foo/a.dart']));
+    }, overrides: contextOverrides);
+
+    testUsingContext('returns multiple files', () async {
+      await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart /foo/b.dart');
+      expect(await readDepfile('a.d'), unorderedEquals(<String>[
+        '/foo/a.dart',
+        '/foo/b.dart',
+      ]));
+    }, overrides: contextOverrides);
+
+    testUsingContext('trims extra spaces between files', () async {
+      await fs.file('a.d').writeAsString('snapshot.d: /foo/a.dart    /foo/b.dart  /foo/c.dart');
+      expect(await readDepfile('a.d'), unorderedEquals(<String>[
+        '/foo/a.dart',
+        '/foo/b.dart',
+        '/foo/c.dart',
+      ]));
+    }, overrides: contextOverrides);
+
+    testUsingContext('returns files with spaces and backslashes', () async {
+      await fs.file('a.d').writeAsString(r'snapshot.d: /foo/a\ a.dart /foo/b\\b.dart /foo/c\\ c.dart');
+      expect(await readDepfile('a.d'), unorderedEquals(<String>[
+        r'/foo/a a.dart',
+        r'/foo/b\b.dart',
+        r'/foo/c\ c.dart',
+      ]));
+    }, overrides: contextOverrides);
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{};
+}
diff --git a/packages/flutter_tools/test/general.shard/base/flags_test.dart b/packages/flutter_tools/test/general.shard/base/flags_test.dart
new file mode 100644
index 0000000..b864a07
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/flags_test.dart
@@ -0,0 +1,93 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/flags.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+typedef _TestMethod = FutureOr<void> Function();
+
+void main() {
+  Cache.disableLocking();
+
+  Future<void> runCommand(Iterable<String> flags, _TestMethod testMethod) async {
+    final List<String> args = <String>['test', ...flags];
+    final _TestCommand command = _TestCommand(testMethod);
+    await createTestCommandRunner(command).run(args);
+  }
+
+  testUsingContext('runCommand works as expected', () async {
+    bool testRan = false;
+    await runCommand(<String>[], () {
+      testRan = true;
+    });
+    expect(testRan, isTrue);
+  });
+
+  group('flags', () {
+    testUsingContext('returns null for undefined flags', () async {
+      await runCommand(<String>[], () {
+        expect(flags['undefined-flag'], isNull);
+      });
+    });
+
+    testUsingContext('picks up default values', () async {
+      await runCommand(<String>[], () {
+        expect(flags['verbose'], isFalse);
+        expect(flags['flag-defaults-to-false'], isFalse);
+        expect(flags['flag-defaults-to-true'], isTrue);
+        expect(flags['option-defaults-to-foo'], 'foo');
+      });
+    });
+
+    testUsingContext('returns null for flags with no default values', () async {
+      await runCommand(<String>[], () {
+        expect(flags['device-id'], isNull);
+        expect(flags['option-no-default'], isNull);
+      });
+    });
+
+    testUsingContext('picks up explicit values', () async {
+      await runCommand(<String>[
+        '--verbose',
+        '--flag-defaults-to-false',
+        '--option-no-default=explicit',
+        '--option-defaults-to-foo=qux',
+      ], () {
+        expect(flags['verbose'], isTrue);
+        expect(flags['flag-defaults-to-false'], isTrue);
+        expect(flags['option-no-default'], 'explicit');
+        expect(flags['option-defaults-to-foo'], 'qux');
+      });
+    });
+  });
+}
+
+class _TestCommand extends FlutterCommand {
+  _TestCommand(this.testMethod) {
+    argParser.addFlag('flag-defaults-to-false', defaultsTo: false);
+    argParser.addFlag('flag-defaults-to-true', defaultsTo: true);
+    argParser.addOption('option-no-default');
+    argParser.addOption('option-defaults-to-foo', defaultsTo: 'foo');
+  }
+
+  final _TestMethod testMethod;
+
+  @override
+  String get name => 'test';
+
+  @override
+  String get description => 'runs a test method';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    await testMethod();
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/base/io_test.dart b/packages/flutter_tools/test/general.shard/base/io_test.dart
new file mode 100644
index 0000000..f70a378
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/io_test.dart
@@ -0,0 +1,34 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('ProcessSignal', () {
+
+    testUsingContext('signals are properly delegated', () async {
+      final MockIoProcessSignal mockSignal = MockIoProcessSignal();
+      final ProcessSignal signalUnderTest = ProcessSignal(mockSignal);
+      final StreamController<io.ProcessSignal> controller = StreamController<io.ProcessSignal>();
+
+      when(mockSignal.watch()).thenAnswer((Invocation invocation) => controller.stream);
+      controller.add(mockSignal);
+
+      expect(signalUnderTest, await signalUnderTest.watch().first);
+    });
+
+    testUsingContext('toString() works', () async {
+      expect(io.ProcessSignal.sigint.toString(), ProcessSignal.SIGINT.toString());
+    });
+  });
+}
+
+class MockIoProcessSignal extends Mock implements io.ProcessSignal {}
diff --git a/packages/flutter_tools/test/general.shard/base/logger_test.dart b/packages/flutter_tools/test/general.shard/base/logger_test.dart
new file mode 100644
index 0000000..3e36fee
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/logger_test.dart
@@ -0,0 +1,742 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert' show jsonEncode;
+
+import 'package:flutter_tools/src/base/context.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/base/terminal.dart';
+import 'package:quiver/testing/async.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+final Generator _kNoAnsiPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+
+void main() {
+  final String red = RegExp.escape(AnsiTerminal.red);
+  final String bold = RegExp.escape(AnsiTerminal.bold);
+  final String resetBold = RegExp.escape(AnsiTerminal.resetBold);
+  final String resetColor = RegExp.escape(AnsiTerminal.resetColor);
+
+  group('AppContext', () {
+    testUsingContext('error', () async {
+      final BufferLogger mockLogger = BufferLogger();
+      final VerboseLogger verboseLogger = VerboseLogger(mockLogger);
+
+      verboseLogger.printStatus('Hey Hey Hey Hey');
+      verboseLogger.printTrace('Oooh, I do I do I do');
+      verboseLogger.printError('Helpless!');
+
+      expect(mockLogger.statusText, matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Hey Hey Hey Hey\n'
+                                             r'\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Oooh, I do I do I do\n$'));
+      expect(mockLogger.traceText, '');
+      expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Helpless!\n$'));
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('ANSI colored errors', () async {
+      final BufferLogger mockLogger = BufferLogger();
+      final VerboseLogger verboseLogger = VerboseLogger(mockLogger);
+
+      verboseLogger.printStatus('Hey Hey Hey Hey');
+      verboseLogger.printTrace('Oooh, I do I do I do');
+      verboseLogger.printError('Helpless!');
+
+      expect(
+          mockLogger.statusText,
+          matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] ' '${bold}Hey Hey Hey Hey$resetBold'
+                  r'\n\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] Oooh, I do I do I do\n$'));
+      expect(mockLogger.traceText, '');
+      expect(
+          mockLogger.errorText,
+          matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] ' '${bold}Helpless!$resetBold$resetColor' r'\n$'));
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+  });
+
+  group('Spinners', () {
+    MockStdio mockStdio;
+    FakeStopwatch mockStopwatch;
+    int called;
+    const List<String> testPlatforms = <String>['linux', 'macos', 'windows', 'fuchsia'];
+    final RegExp secondDigits = RegExp(r'[0-9,.]*[0-9]m?s');
+
+    AnsiStatus _createAnsiStatus() {
+      mockStopwatch = FakeStopwatch();
+      return AnsiStatus(
+        message: 'Hello world',
+        timeout: const Duration(seconds: 2),
+        padding: 20,
+        onFinish: () => called += 1,
+      );
+    }
+
+    setUp(() {
+      mockStdio = MockStdio();
+      called = 0;
+    });
+
+    List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
+    List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
+
+    void doWhileAsync(FakeAsync time, bool doThis()) {
+      do {
+        time.elapse(const Duration(milliseconds: 1));
+      } while (doThis());
+    }
+
+    for (String testOs in testPlatforms) {
+      testUsingContext('AnsiSpinner works for $testOs (1)', () async {
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          final AnsiSpinner ansiSpinner = AnsiSpinner(
+            timeout: const Duration(hours: 10),
+          )..start();
+          doWhileAsync(time, () => ansiSpinner.ticks < 10);
+          List<String> lines = outputStdout();
+          expect(lines[0], startsWith(
+            platform.isWindows
+              ? ' \b\\\b|\b/\b-\b\\\b|\b/\b-'
+              : ' \b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
+            ),
+          );
+          expect(lines[0].endsWith('\n'), isFalse);
+          expect(lines.length, equals(1));
+          ansiSpinner.stop();
+          lines = outputStdout();
+          expect(lines[0], endsWith('\b \b'));
+          expect(lines.length, equals(1));
+
+          // Verify that stopping or canceling multiple times throws.
+          expect(() {
+            ansiSpinner.stop();
+          }, throwsA(isInstanceOf<AssertionError>()));
+          expect(() {
+            ansiSpinner.cancel();
+          }, throwsA(isInstanceOf<AssertionError>()));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: testOs),
+        Stdio: () => mockStdio,
+      });
+
+      testUsingContext('AnsiSpinner works for $testOs (2)', () async {
+        bool done = false;
+        mockStopwatch = FakeStopwatch();
+        FakeAsync().run((FakeAsync time) {
+          final AnsiSpinner ansiSpinner = AnsiSpinner(
+            timeout: const Duration(seconds: 2),
+          )..start();
+          mockStopwatch.elapsed = const Duration(seconds: 1);
+          doWhileAsync(time, () => ansiSpinner.ticks < 10); // one second
+          expect(ansiSpinner.seemsSlow, isFalse);
+          expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.')));
+          mockStopwatch.elapsed = const Duration(seconds: 3);
+          doWhileAsync(time, () => ansiSpinner.ticks < 30); // three seconds
+          expect(ansiSpinner.seemsSlow, isTrue);
+          // Check the 2nd line to verify there's a newline before the warning
+          expect(outputStdout()[1], contains('This is taking an unexpectedly long time.'));
+          ansiSpinner.stop();
+          expect(outputStdout().join('\n'), isNot(contains('(!)')));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: testOs),
+        Stdio: () => mockStdio,
+        Stopwatch: () => mockStopwatch,
+      });
+
+      testUsingContext('Stdout startProgress on colored terminal for $testOs', () async {
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          final Logger logger = context.get<Logger>();
+          final Status status = logger.startProgress(
+            'Hello',
+            progressId: null,
+            timeout: timeoutConfiguration.slowOperation,
+            progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
+          );
+          expect(outputStderr().length, equals(1));
+          expect(outputStderr().first, isEmpty);
+          // the 5 below is the margin that is always included between the message and the time.
+          expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\$' :
+                                                                         r'^Hello {15} {5} {8}[\b]{8} {7}⣽$'));
+          status.stop();
+          expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$' :
+                                                                         r'^Hello {15} {5} {8}[\b]{8} {7}⣽[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$'));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Logger: () => StdoutLogger(),
+        OutputPreferences: () => OutputPreferences(showColor: true),
+        Platform: () => FakePlatform(operatingSystem: testOs)..stdoutSupportsAnsi = true,
+        Stdio: () => mockStdio,
+      });
+
+      testUsingContext('Stdout startProgress on colored terminal pauses on $testOs', () async {
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          final Logger logger = context.get<Logger>();
+          final Status status = logger.startProgress(
+            'Knock Knock, Who\'s There',
+            timeout: const Duration(days: 10),
+            progressIndicatorPadding: 10,
+          );
+          logger.printStatus('Rude Interrupting Cow');
+          status.stop();
+          final String a = platform.isWindows ? '\\' : '⣽';
+          final String b = platform.isWindows ? '|' : '⣻';
+          expect(
+            outputStdout().join('\n'),
+            'Knock Knock, Who\'s There     ' // initial message
+            '        ' // placeholder so that spinner can backspace on its first tick
+            '\b\b\b\b\b\b\b\b       $a' // first tick
+            '\b\b\b\b\b\b\b\b        ' // clearing the spinner
+            '\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
+            '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                             ' // clearing the message
+            '\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' // clearing the clearing of the message
+            'Rude Interrupting Cow\n' // message
+            'Knock Knock, Who\'s There     ' // message restoration
+            '        ' // placeholder so that spinner can backspace on its second tick
+            '\b\b\b\b\b\b\b\b       $b' // second tick
+            '\b\b\b\b\b\b\b\b        ' // clearing the spinner to put the time
+            '\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
+            '    0.0s\n', // replacing it with the time
+          );
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Logger: () => StdoutLogger(),
+        OutputPreferences: () => OutputPreferences(showColor: true),
+        Platform: () => FakePlatform(operatingSystem: testOs)..stdoutSupportsAnsi = true,
+        Stdio: () => mockStdio,
+      });
+
+      testUsingContext('AnsiStatus works for $testOs', () {
+        final AnsiStatus ansiStatus = _createAnsiStatus();
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          ansiStatus.start();
+          mockStopwatch.elapsed = const Duration(seconds: 1);
+          doWhileAsync(time, () => ansiStatus.ticks < 10); // one second
+          expect(ansiStatus.seemsSlow, isFalse);
+          expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.')));
+          expect(outputStdout().join('\n'), isNot(contains('(!)')));
+          mockStopwatch.elapsed = const Duration(seconds: 3);
+          doWhileAsync(time, () => ansiStatus.ticks < 30); // three seconds
+          expect(ansiStatus.seemsSlow, isTrue);
+          expect(outputStdout().join('\n'), contains('This is taking an unexpectedly long time.'));
+
+          // Test that the number of '\b' is correct.
+          for (String line in outputStdout()) {
+            int currLength = 0;
+            for (int i = 0; i < line.length; i += 1) {
+              currLength += line[i] == '\b' ? -1 : 1;
+              expect(currLength, isNonNegative, reason: 'The following line has overflow backtraces:\n' + jsonEncode(line));
+            }
+          }
+
+          ansiStatus.stop();
+          expect(outputStdout().join('\n'), contains('(!)'));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: testOs),
+        Stdio: () => mockStdio,
+        Stopwatch: () => mockStopwatch,
+      });
+
+      testUsingContext('AnsiStatus works when canceled for $testOs', () async {
+        final AnsiStatus ansiStatus = _createAnsiStatus();
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          ansiStatus.start();
+          mockStopwatch.elapsed = const Duration(seconds: 1);
+          doWhileAsync(time, () => ansiStatus.ticks < 10);
+          List<String> lines = outputStdout();
+          expect(lines[0], startsWith(platform.isWindows
+              ? 'Hello world                      \b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |\b\b\b\b\b\b\b\b       /\b\b\b\b\b\b\b\b       -\b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |\b\b\b\b\b\b\b\b       /\b\b\b\b\b\b\b\b       -\b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |'
+              : 'Hello world                      \b\b\b\b\b\b\b\b       ⣽\b\b\b\b\b\b\b\b       ⣻\b\b\b\b\b\b\b\b       ⢿\b\b\b\b\b\b\b\b       ⡿\b\b\b\b\b\b\b\b       ⣟\b\b\b\b\b\b\b\b       ⣯\b\b\b\b\b\b\b\b       ⣷\b\b\b\b\b\b\b\b       ⣾\b\b\b\b\b\b\b\b       ⣽\b\b\b\b\b\b\b\b       ⣻'));
+          expect(lines.length, equals(1));
+          expect(lines[0].endsWith('\n'), isFalse);
+
+          // Verify a cancel does _not_ print the time and prints a newline.
+          ansiStatus.cancel();
+          lines = outputStdout();
+          final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
+          expect(matches, isEmpty);
+          final String x = platform.isWindows ? '|' : '⣻';
+          expect(lines[0], endsWith('$x\b\b\b\b\b\b\b\b        \b\b\b\b\b\b\b\b'));
+          expect(called, equals(1));
+          expect(lines.length, equals(2));
+          expect(lines[1], equals(''));
+
+          // Verify that stopping or canceling multiple times throws.
+          expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+          expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: testOs),
+        Stdio: () => mockStdio,
+        Stopwatch: () => mockStopwatch,
+      });
+
+      testUsingContext('AnsiStatus works when stopped for $testOs', () async {
+        final AnsiStatus ansiStatus = _createAnsiStatus();
+        bool done = false;
+        FakeAsync().run((FakeAsync time) {
+          ansiStatus.start();
+          mockStopwatch.elapsed = const Duration(seconds: 1);
+          doWhileAsync(time, () => ansiStatus.ticks < 10);
+          List<String> lines = outputStdout();
+          expect(lines, hasLength(1));
+          expect(lines[0],
+            platform.isWindows
+              ? 'Hello world                      \b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |\b\b\b\b\b\b\b\b       /\b\b\b\b\b\b\b\b       -\b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |\b\b\b\b\b\b\b\b       /\b\b\b\b\b\b\b\b       -\b\b\b\b\b\b\b\b       \\\b\b\b\b\b\b\b\b       |'
+              : 'Hello world                      \b\b\b\b\b\b\b\b       ⣽\b\b\b\b\b\b\b\b       ⣻\b\b\b\b\b\b\b\b       ⢿\b\b\b\b\b\b\b\b       ⡿\b\b\b\b\b\b\b\b       ⣟\b\b\b\b\b\b\b\b       ⣯\b\b\b\b\b\b\b\b       ⣷\b\b\b\b\b\b\b\b       ⣾\b\b\b\b\b\b\b\b       ⣽\b\b\b\b\b\b\b\b       ⣻',
+          );
+
+          // Verify a stop prints the time.
+          ansiStatus.stop();
+          lines = outputStdout();
+          expect(lines, hasLength(2));
+          expect(lines[0], matches(
+            platform.isWindows
+              ? r'Hello world               {8}[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7} [\b]{8}[\d., ]{6}[\d]ms$'
+              : r'Hello world               {8}[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7}⢿[\b]{8} {7}⡿[\b]{8} {7}⣟[\b]{8} {7}⣯[\b]{8} {7}⣷[\b]{8} {7}⣾[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7} [\b]{8}[\d., ]{5}[\d]ms$'
+          ));
+          expect(lines[1], isEmpty);
+          final List<Match> times = secondDigits.allMatches(lines[0]).toList();
+          expect(times, isNotNull);
+          expect(times, hasLength(1));
+          final Match match = times.single;
+          expect(lines[0], endsWith(match.group(0)));
+          expect(called, equals(1));
+          expect(lines.length, equals(2));
+          expect(lines[1], equals(''));
+
+          // Verify that stopping or canceling multiple times throws.
+          expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+          expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+          done = true;
+        });
+        expect(done, isTrue);
+      }, overrides: <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: testOs),
+        Stdio: () => mockStdio,
+        Stopwatch: () => mockStopwatch,
+      });
+    }
+  });
+  group('Output format', () {
+    MockStdio mockStdio;
+    SummaryStatus summaryStatus;
+    int called;
+    final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
+
+    setUp(() {
+      mockStdio = MockStdio();
+      called = 0;
+      summaryStatus = SummaryStatus(
+        message: 'Hello world',
+        timeout: timeoutConfiguration.slowOperation,
+        padding: 20,
+        onFinish: () => called++,
+      );
+    });
+
+    List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
+    List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
+
+    testUsingContext('Error logs are wrapped', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printError('0123456789' * 15);
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines[0], equals('0123456789' * 4));
+      expect(lines[1], equals('0123456789' * 4));
+      expect(lines[2], equals('0123456789' * 4));
+      expect(lines[3], equals('0123456789' * 3));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Error logs are wrapped and can be indented.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printError('0123456789' * 15, indent: 5);
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('     01234567890123456789012345678901234'));
+      expect(lines[1], equals('     56789012345678901234567890123456789'));
+      expect(lines[2], equals('     01234567890123456789012345678901234'));
+      expect(lines[3], equals('     56789012345678901234567890123456789'));
+      expect(lines[4], equals('     0123456789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Error logs are wrapped and can have hanging indent.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printError('0123456789' * 15, hangingIndent: 5);
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('0123456789012345678901234567890123456789'));
+      expect(lines[1], equals('     01234567890123456789012345678901234'));
+      expect(lines[2], equals('     56789012345678901234567890123456789'));
+      expect(lines[3], equals('     01234567890123456789012345678901234'));
+      expect(lines[4], equals('     56789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Error logs are wrapped, indented, and can have hanging indent.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printError('0123456789' * 15, indent: 4, hangingIndent: 5);
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('    012345678901234567890123456789012345'));
+      expect(lines[1], equals('         6789012345678901234567890123456'));
+      expect(lines[2], equals('         7890123456789012345678901234567'));
+      expect(lines[3], equals('         8901234567890123456789012345678'));
+      expect(lines[4], equals('         901234567890123456789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Stdout logs are wrapped', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus('0123456789' * 15);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals('0123456789' * 4));
+      expect(lines[1], equals('0123456789' * 4));
+      expect(lines[2], equals('0123456789' * 4));
+      expect(lines[3], equals('0123456789' * 3));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Stdout logs are wrapped and can be indented.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus('0123456789' * 15, indent: 5);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('     01234567890123456789012345678901234'));
+      expect(lines[1], equals('     56789012345678901234567890123456789'));
+      expect(lines[2], equals('     01234567890123456789012345678901234'));
+      expect(lines[3], equals('     56789012345678901234567890123456789'));
+      expect(lines[4], equals('     0123456789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Stdout logs are wrapped and can have hanging indent.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus('0123456789' * 15, hangingIndent: 5);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('0123456789012345678901234567890123456789'));
+      expect(lines[1], equals('     01234567890123456789012345678901234'));
+      expect(lines[2], equals('     56789012345678901234567890123456789'));
+      expect(lines[3], equals('     01234567890123456789012345678901234'));
+      expect(lines[4], equals('     56789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus('0123456789' * 15, indent: 4, hangingIndent: 5);
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines.length, equals(6));
+      expect(lines[0], equals('    012345678901234567890123456789012345'));
+      expect(lines[1], equals('         6789012345678901234567890123456'));
+      expect(lines[2], equals('         7890123456789012345678901234567'));
+      expect(lines[3], equals('         8901234567890123456789012345678'));
+      expect(lines[4], equals('         901234567890123456789'));
+      expect(lines[5], isEmpty);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Error logs are red', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printError('Pants on fire!');
+      final List<String> lines = outputStderr();
+      expect(outputStdout().length, equals(1));
+      expect(outputStdout().first, isEmpty);
+      expect(lines[0], equals('${AnsiTerminal.red}Pants on fire!${AnsiTerminal.resetColor}'));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('Stdout logs are not colored', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus('All good.');
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals('All good.'));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus(
+        null,
+        emphasis: null,
+        color: null,
+        newline: null,
+        indent: null,
+      );
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals(''));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('Stdout printStatus handle null inputs on non-color terminal', () async {
+      final Logger logger = context.get<Logger>();
+      logger.printStatus(
+        null,
+        emphasis: null,
+        color: null,
+        newline: null,
+        indent: null,
+      );
+      final List<String> lines = outputStdout();
+      expect(outputStderr().length, equals(1));
+      expect(outputStderr().first, isEmpty);
+      expect(lines[0], equals(''));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('Stdout startProgress on non-color terminal', () async {
+      bool done = false;
+      FakeAsync().run((FakeAsync time) {
+        final Logger logger = context.get<Logger>();
+        final Status status = logger.startProgress(
+          'Hello',
+          progressId: null,
+          timeout: timeoutConfiguration.slowOperation,
+          progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
+        );
+        expect(outputStderr().length, equals(1));
+        expect(outputStderr().first, isEmpty);
+        // the 5 below is the margin that is always included between the message and the time.
+        expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}$' :
+                                                                       r'^Hello {15} {5}$'));
+        status.stop();
+        expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$' :
+                                                                       r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$'));
+        done = true;
+      });
+      expect(done, isTrue);
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('SummaryStatus works when canceled', () async {
+      summaryStatus.start();
+      List<String> lines = outputStdout();
+      expect(lines[0], startsWith('Hello world              '));
+      expect(lines.length, equals(1));
+      expect(lines[0].endsWith('\n'), isFalse);
+
+      // Verify a cancel does _not_ print the time and prints a newline.
+      summaryStatus.cancel();
+      lines = outputStdout();
+      final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
+      expect(matches, isEmpty);
+      expect(lines[0], endsWith(' '));
+      expect(called, equals(1));
+      expect(lines.length, equals(2));
+      expect(lines[1], equals(''));
+
+      // Verify that stopping or canceling multiple times throws.
+      expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+      expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+    }, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform});
+
+    testUsingContext('SummaryStatus works when stopped', () async {
+      summaryStatus.start();
+      List<String> lines = outputStdout();
+      expect(lines[0], startsWith('Hello world              '));
+      expect(lines.length, equals(1));
+
+      // Verify a stop prints the time.
+      summaryStatus.stop();
+      lines = outputStdout();
+      final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
+      expect(matches, isNotNull);
+      expect(matches, hasLength(1));
+      final Match match = matches.first;
+      expect(lines[0], endsWith(match.group(0)));
+      expect(called, equals(1));
+      expect(lines.length, equals(2));
+      expect(lines[1], equals(''));
+
+      // Verify that stopping or canceling multiple times throws.
+      expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>()));
+      expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>()));
+    }, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform});
+
+    testUsingContext('sequential startProgress calls with StdoutLogger', () async {
+      final Logger logger = context.get<Logger>();
+      logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop();
+      logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop();
+      final List<String> output = outputStdout();
+      expect(output.length, equals(3));
+      // There's 61 spaces at the start: 59 (padding default) - 3 (length of AAA) + 5 (margin).
+      // Then there's a left-padded "0ms" 8 characters wide, so 5 spaces then "0ms"
+      // (except sometimes it's randomly slow so we handle up to "99,999ms").
+      expect(output[0], matches(RegExp(r'AAA[ ]{61}[\d, ]{5}[\d]ms')));
+      expect(output[1], matches(RegExp(r'BBB[ ]{61}[\d, ]{5}[\d]ms')));
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
+      final Logger logger = context.get<Logger>();
+      logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop();
+      logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop();
+      expect(outputStdout(), <Matcher>[
+        matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] AAA$'),
+        matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] AAA \(completed.*\)$'),
+        matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] BBB$'),
+        matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms|       )\] BBB \(completed.*\)$'),
+        matches(r'^$'),
+      ]);
+    }, overrides: <Type, Generator>{
+      Logger: () => VerboseLogger(StdoutLogger()),
+      Stdio: () => mockStdio,
+      Platform: _kNoAnsiPlatform,
+    });
+
+    testUsingContext('sequential startProgress calls with BufferLogger', () async {
+      final BufferLogger logger = context.get<Logger>();
+      logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop();
+      logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop();
+      expect(logger.statusText, 'AAA\nBBB\n');
+    }, overrides: <Type, Generator>{
+      Logger: () => BufferLogger(),
+      Platform: _kNoAnsiPlatform,
+    });
+  });
+}
+
+class FakeStopwatch implements Stopwatch {
+  @override
+  bool get isRunning => _isRunning;
+  bool _isRunning = false;
+
+  @override
+  void start() => _isRunning = true;
+
+  @override
+  void stop() => _isRunning = false;
+
+  @override
+  Duration elapsed = Duration.zero;
+
+  @override
+  int get elapsedMicroseconds => elapsed.inMicroseconds;
+
+  @override
+  int get elapsedMilliseconds => elapsed.inMilliseconds;
+
+  @override
+  int get elapsedTicks => elapsed.inMilliseconds;
+
+  @override
+  int get frequency => 1000;
+
+  @override
+  void reset() {
+    _isRunning = false;
+    elapsed = Duration.zero;
+  }
+
+  @override
+  String toString() => '$runtimeType $elapsed $isRunning';
+}
diff --git a/packages/flutter_tools/test/general.shard/base/logs_test.dart b/packages/flutter_tools/test/general.shard/base/logs_test.dart
new file mode 100644
index 0000000..f0f3ad0
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/logs_test.dart
@@ -0,0 +1,25 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/commands/logs.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('logs', () {
+    testUsingContext('fail with a bad device id', () async {
+      final LogsCommand command = LogsCommand();
+      applyMocksToCommand(command);
+      try {
+        await createTestCommandRunner(command).run(<String>['-d', 'abc123', 'logs']);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 1);
+      }
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/net_test.dart b/packages/flutter_tools/test/general.shard/base/net_test.dart
new file mode 100644
index 0000000..a7dc3c0
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/net_test.dart
@@ -0,0 +1,265 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:flutter_tools/src/base/io.dart' as io;
+import 'package:flutter_tools/src/base/net.dart';
+import 'package:quiver/testing/async.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  testUsingContext('retry from 500', () async {
+    String error;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic exception) {
+        error = 'test failed unexpectedly: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n',
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(500),
+  });
+
+  testUsingContext('retry from network error', () async {
+    String error;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic exception) {
+        error = 'test failed unexpectedly: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n',
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(200),
+  });
+
+  testUsingContext('retry from SocketException', () async {
+    String error;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic exception) {
+        error = 'test failed unexpectedly: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n',
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+    expect(testLogger.traceText, contains('Download error: SocketException'));
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClientThrowing(
+      const io.SocketException('test exception handling'),
+    ),
+  });
+
+  testUsingContext('no retry from HandshakeException', () async {
+    String error;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic exception) {
+        error = 'test failed: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText, '');
+    });
+    expect(error, startsWith('test failed'));
+    expect(testLogger.traceText, contains('HandshakeException'));
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClientThrowing(
+      const io.HandshakeException('test exception handling'),
+    ),
+  });
+
+testUsingContext('retry from HttpException', () async {
+    String error;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic exception) {
+        error = 'test failed unexpectedly: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n',
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+    expect(testLogger.traceText, contains('Download error: HttpException'));
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClientThrowing(
+      const io.HttpException('test exception handling'),
+    ),
+  });
+
+  testUsingContext('max attempts', () async {
+    String error;
+    List<int> actualResult;
+    FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/'), maxAttempts: 3).then((List<int> value) {
+        actualResult = value;
+      }, onError: (dynamic exception) {
+        error = 'test failed unexpectedly: $exception';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- retry 3\n',
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+    expect(actualResult, isNull);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(500),
+  });
+
+  testUsingContext('remote file non-existant', () async {
+    final Uri invalid = Uri.parse('http://example.invalid/');
+    final bool result = await doesRemoteFileExist(invalid);
+    expect(result, false);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(404),
+  });
+
+  testUsingContext('remote file server error', () async {
+    final Uri valid = Uri.parse('http://example.valid/');
+    final bool result = await doesRemoteFileExist(valid);
+    expect(result, false);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(500),
+  });
+
+  testUsingContext('remote file exists', () async {
+    final Uri valid = Uri.parse('http://example.valid/');
+    final bool result = await doesRemoteFileExist(valid);
+    expect(result, true);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(200),
+  });
+}
+
+class MockHttpClientThrowing implements io.HttpClient {
+  MockHttpClientThrowing(this.exception);
+
+  final Exception exception;
+
+  @override
+  Future<io.HttpClientRequest> getUrl(Uri url) async {
+    throw exception;
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClient - $invocation';
+  }
+}
+
+class MockHttpClient implements io.HttpClient {
+  MockHttpClient(this.statusCode);
+
+  final int statusCode;
+
+  @override
+  Future<io.HttpClientRequest> getUrl(Uri url) async {
+    return MockHttpClientRequest(statusCode);
+  }
+
+  @override
+  Future<io.HttpClientRequest> headUrl(Uri url) async {
+    return MockHttpClientRequest(statusCode);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClient - $invocation';
+  }
+}
+
+class MockHttpClientRequest implements io.HttpClientRequest {
+  MockHttpClientRequest(this.statusCode);
+
+  final int statusCode;
+
+  @override
+  Future<io.HttpClientResponse> close() async {
+    return MockHttpClientResponse(statusCode);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientRequest - $invocation';
+  }
+}
+
+class MockHttpClientResponse implements io.HttpClientResponse {
+  MockHttpClientResponse(this.statusCode);
+
+  @override
+  final int statusCode;
+
+  @override
+  String get reasonPhrase => '<reason phrase>';
+
+  @override
+  StreamSubscription<Uint8List> listen(
+    void onData(Uint8List event), {
+    Function onError,
+    void onDone(),
+    bool cancelOnError,
+  }) {
+    return Stream<Uint8List>.fromFuture(Future<Uint8List>.error(const io.SocketException('test')))
+      .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+  }
+
+  @override
+  Future<dynamic> forEach(void Function(Uint8List element) action) {
+    return Future<void>.error(const io.SocketException('test'));
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientResponse - $invocation';
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/base/os_test.dart b/packages/flutter_tools/test/general.shard/base/os_test.dart
new file mode 100644
index 0000000..04cf9ac
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/os_test.dart
@@ -0,0 +1,99 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:platform/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+const String kExecutable = 'foo';
+const String kPath1 = '/bar/bin/$kExecutable';
+const String kPath2 = '/another/bin/$kExecutable';
+
+void main() {
+  ProcessManager mockProcessManager;
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+  });
+
+  group('which on POSIX', () {
+
+    testUsingContext('returns null when executable does not exist', () async {
+      when(mockProcessManager.runSync(<String>['which', kExecutable]))
+          .thenReturn(ProcessResult(0, 1, null, null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      expect(utils.which(kExecutable), isNull);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+
+    testUsingContext('returns exactly one result', () async {
+      when(mockProcessManager.runSync(<String>['which', 'foo']))
+          .thenReturn(ProcessResult(0, 0, kPath1, null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      expect(utils.which(kExecutable).path, kPath1);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+
+    testUsingContext('returns all results for whichAll', () async {
+      when(mockProcessManager.runSync(<String>['which', '-a', kExecutable]))
+          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      final List<File> result = utils.whichAll(kExecutable);
+      expect(result, hasLength(2));
+      expect(result[0].path, kPath1);
+      expect(result[1].path, kPath2);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'linux'),
+    });
+  });
+
+  group('which on Windows', () {
+
+    testUsingContext('returns null when executable does not exist', () async {
+      when(mockProcessManager.runSync(<String>['where', kExecutable]))
+          .thenReturn(ProcessResult(0, 1, null, null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      expect(utils.which(kExecutable), isNull);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'windows'),
+    });
+
+    testUsingContext('returns exactly one result', () async {
+      when(mockProcessManager.runSync(<String>['where', 'foo']))
+          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      expect(utils.which(kExecutable).path, kPath1);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'windows'),
+    });
+
+    testUsingContext('returns all results for whichAll', () async {
+      when(mockProcessManager.runSync(<String>['where', kExecutable]))
+          .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null));
+      final OperatingSystemUtils utils = OperatingSystemUtils();
+      final List<File> result = utils.whichAll(kExecutable);
+      expect(result, hasLength(2));
+      expect(result[0].path, kPath1);
+      expect(result[1].path, kPath2);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(operatingSystem: 'windows'),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/base/os_utils_test.dart b/packages/flutter_tools/test/general.shard/base/os_utils_test.dart
new file mode 100644
index 0000000..a3e7714
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/os_utils_test.dart
@@ -0,0 +1,39 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('OperatingSystemUtils', () {
+    Directory tempDir;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_os_utils_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('makeExecutable', () async {
+      final File file = fs.file(fs.path.join(tempDir.path, 'foo.script'));
+      file.writeAsStringSync('hello world');
+      os.makeExecutable(file);
+
+      // Skip this test on windows.
+      if (!platform.isWindows) {
+        final String mode = file.statSync().modeString();
+        // rwxr--r--
+        expect(mode.substring(0, 3), endsWith('x'));
+      }
+    }, overrides: <Type, Generator>{
+      OperatingSystemUtils: () => OperatingSystemUtils(),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base/process_test.dart b/packages/flutter_tools/test/general.shard/base/process_test.dart
new file mode 100644
index 0000000..8ffca5e
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/process_test.dart
@@ -0,0 +1,95 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+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/base/process.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart' show MockProcess, MockProcessManager;
+
+void main() {
+  group('process exceptions', () {
+    ProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = PlainMockProcessManager();
+    });
+
+    testUsingContext('runCheckedAsync exceptions should be ProcessException objects', () async {
+      when(mockProcessManager.run(<String>['false']))
+          .thenAnswer((Invocation invocation) => Future<ProcessResult>.value(ProcessResult(0, 1, '', '')));
+      expect(() async => await runCheckedAsync(<String>['false']), throwsA(isInstanceOf<ProcessException>()));
+    }, overrides: <Type, Generator>{ProcessManager: () => mockProcessManager});
+  });
+  group('shutdownHooks', () {
+    testUsingContext('runInExpectedOrder', () async {
+      int i = 1;
+      int serializeRecording1;
+      int serializeRecording2;
+      int postProcessRecording;
+      int cleanup;
+
+      addShutdownHook(() async {
+        serializeRecording1 = i++;
+      }, ShutdownStage.SERIALIZE_RECORDING);
+
+      addShutdownHook(() async {
+        cleanup = i++;
+      }, ShutdownStage.CLEANUP);
+
+      addShutdownHook(() async {
+        postProcessRecording = i++;
+      }, ShutdownStage.POST_PROCESS_RECORDING);
+
+      addShutdownHook(() async {
+        serializeRecording2 = i++;
+      }, ShutdownStage.SERIALIZE_RECORDING);
+
+      await runShutdownHooks();
+
+      expect(serializeRecording1, lessThanOrEqualTo(2));
+      expect(serializeRecording2, lessThanOrEqualTo(2));
+      expect(postProcessRecording, 3);
+      expect(cleanup, 4);
+    });
+  });
+  group('output formatting', () {
+    MockProcessManager mockProcessManager;
+    BufferLogger mockLogger;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockLogger = BufferLogger();
+    });
+
+    MockProcess Function(List<String>) processMetaFactory(List<String> stdout, { List<String> stderr = const <String>[] }) {
+      final Stream<List<int>> stdoutStream =
+          Stream<List<int>>.fromIterable(stdout.map<List<int>>((String s) => s.codeUnits));
+      final Stream<List<int>> stderrStream =
+      Stream<List<int>>.fromIterable(stderr.map<List<int>>((String s) => s.codeUnits));
+      return (List<String> command) => MockProcess(stdout: stdoutStream, stderr: stderrStream);
+    }
+
+    testUsingContext('Command output is not wrapped.', () async {
+      final List<String> testString = <String>['0123456789' * 10];
+      mockProcessManager.processFactory = processMetaFactory(testString, stderr: testString);
+      await runCommandAndStreamOutput(<String>['command']);
+      expect(mockLogger.statusText, equals('${testString[0]}\n'));
+      expect(mockLogger.errorText, equals('${testString[0]}\n'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
+      Platform: () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false,
+    });
+  });
+}
+
+class PlainMockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/base/terminal_test.dart b/packages/flutter_tools/test/general.shard/base/terminal_test.dart
new file mode 100644
index 0000000..f189d2b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/terminal_test.dart
@@ -0,0 +1,171 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/globals.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('output preferences', () {
+    testUsingContext('can wrap output', () async {
+      printStatus('0123456789' * 8);
+      expect(testLogger.statusText, equals(('0123456789' * 4 + '\n') * 2));
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
+    });
+
+    testUsingContext('can turn off wrapping', () async {
+      final String testString = '0123456789' * 20;
+      printStatus(testString);
+      expect(testLogger.statusText, equals('$testString\n'));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+  });
+
+  group('ANSI coloring and bold', () {
+    AnsiTerminal terminal;
+
+    setUp(() {
+      terminal = AnsiTerminal();
+    });
+
+    testUsingContext('adding colors works', () {
+      for (TerminalColor color in TerminalColor.values) {
+        expect(
+          terminal.color('output', color),
+          equals('${AnsiTerminal.colorCode(color)}output${AnsiTerminal.resetColor}'),
+        );
+      }
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+
+    testUsingContext('adding bold works', () {
+      expect(
+        terminal.bolden('output'),
+        equals('${AnsiTerminal.bold}output${AnsiTerminal.resetBold}'),
+      );
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+
+    testUsingContext('nesting bold within color works', () {
+      expect(
+        terminal.color(terminal.bolden('output'), TerminalColor.blue),
+        equals('${AnsiTerminal.blue}${AnsiTerminal.bold}output${AnsiTerminal.resetBold}${AnsiTerminal.resetColor}'),
+      );
+      expect(
+        terminal.color('non-bold ${terminal.bolden('output')} also non-bold', TerminalColor.blue),
+        equals('${AnsiTerminal.blue}non-bold ${AnsiTerminal.bold}output${AnsiTerminal.resetBold} also non-bold${AnsiTerminal.resetColor}'),
+      );
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+
+    testUsingContext('nesting color within bold works', () {
+      expect(
+        terminal.bolden(terminal.color('output', TerminalColor.blue)),
+        equals('${AnsiTerminal.bold}${AnsiTerminal.blue}output${AnsiTerminal.resetColor}${AnsiTerminal.resetBold}'),
+      );
+      expect(
+        terminal.bolden('non-color ${terminal.color('output', TerminalColor.blue)} also non-color'),
+        equals('${AnsiTerminal.bold}non-color ${AnsiTerminal.blue}output${AnsiTerminal.resetColor} also non-color${AnsiTerminal.resetBold}'),
+      );
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+
+    testUsingContext('nesting color within color works', () {
+      expect(
+        terminal.color(terminal.color('output', TerminalColor.blue), TerminalColor.magenta),
+        equals('${AnsiTerminal.magenta}${AnsiTerminal.blue}output${AnsiTerminal.resetColor}${AnsiTerminal.magenta}${AnsiTerminal.resetColor}'),
+      );
+      expect(
+        terminal.color('magenta ${terminal.color('output', TerminalColor.blue)} also magenta', TerminalColor.magenta),
+        equals('${AnsiTerminal.magenta}magenta ${AnsiTerminal.blue}output${AnsiTerminal.resetColor}${AnsiTerminal.magenta} also magenta${AnsiTerminal.resetColor}'),
+      );
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+
+    testUsingContext('nesting bold within bold works', () {
+      expect(
+        terminal.bolden(terminal.bolden('output')),
+        equals('${AnsiTerminal.bold}output${AnsiTerminal.resetBold}'),
+      );
+      expect(
+        terminal.bolden('bold ${terminal.bolden('output')} still bold'),
+        equals('${AnsiTerminal.bold}bold output still bold${AnsiTerminal.resetBold}'),
+      );
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(showColor: true),
+      Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
+    });
+  });
+
+  group('character input prompt', () {
+    AnsiTerminal terminalUnderTest;
+
+    setUp(() {
+      terminalUnderTest = TestTerminal();
+    });
+
+    testUsingContext('character prompt', () async {
+      mockStdInStream = Stream<String>.fromFutures(<Future<String>>[
+        Future<String>.value('d'), // Not in accepted list.
+        Future<String>.value('\n'), // Not in accepted list
+        Future<String>.value('b'),
+      ]).asBroadcastStream();
+      final String choice = await terminalUnderTest.promptForCharInput(
+        <String>['a', 'b', 'c'],
+        prompt: 'Please choose something',
+      );
+      expect(choice, 'b');
+      expect(
+          testLogger.statusText,
+          'Please choose something [a|b|c]: d\n'
+          'Please choose something [a|b|c]: \n'
+          '\n'
+          'Please choose something [a|b|c]: b\n');
+    });
+
+    testUsingContext('default character choice without displayAcceptedCharacters', () async {
+      mockStdInStream = Stream<String>.fromFutures(<Future<String>>[
+        Future<String>.value('\n'), // Not in accepted list
+      ]).asBroadcastStream();
+      final String choice = await terminalUnderTest.promptForCharInput(
+        <String>['a', 'b', 'c'],
+        prompt: 'Please choose something',
+        displayAcceptedCharacters: false,
+        defaultChoiceIndex: 1, // which is b.
+      );
+      expect(choice, 'b');
+      expect(
+          testLogger.statusText,
+          'Please choose something: \n'
+          '\n');
+    });
+  });
+}
+
+Stream<String> mockStdInStream;
+
+class TestTerminal extends AnsiTerminal {
+  @override
+  Stream<String> get keystrokes {
+    return mockStdInStream;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/base/utils_test.dart b/packages/flutter_tools/test/general.shard/base/utils_test.dart
new file mode 100644
index 0000000..097d72f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base/utils_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/utils.dart';
+import 'package:platform/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('BotDetector', () {
+    FakePlatform fakePlatform;
+    MockStdio mockStdio;
+    BotDetector botDetector;
+
+    setUp(() {
+      fakePlatform = FakePlatform()..environment = <String, String>{};
+      mockStdio = MockStdio();
+      botDetector = const BotDetector();
+    });
+
+    group('isRunningOnBot', () {
+      testUsingContext('returns false unconditionally if BOT=false is set', () async {
+        fakePlatform.environment['BOT'] = 'false';
+        fakePlatform.environment['TRAVIS'] = 'true';
+        expect(botDetector.isRunningOnBot, isFalse);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('returns false unconditionally if FLUTTER_HOST is set', () async {
+        fakePlatform.environment['FLUTTER_HOST'] = 'foo';
+        fakePlatform.environment['TRAVIS'] = 'true';
+        expect(botDetector.isRunningOnBot, isFalse);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+
+      testUsingContext('returns true for non-interactive terminals', () async {
+        mockStdio.stdout.hasTerminal = true;
+        expect(botDetector.isRunningOnBot, isFalse);
+        mockStdio.stdout.hasTerminal = false;
+        expect(botDetector.isRunningOnBot, isTrue);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/base_utils_test.dart b/packages/flutter_tools/test/general.shard/base_utils_test.dart
new file mode 100644
index 0000000..6e1192d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/base_utils_test.dart
@@ -0,0 +1,36 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/utils.dart';
+
+import '../src/common.dart';
+
+void main() {
+  group('ItemListNotifier', () {
+    test('sends notifications', () async {
+      final ItemListNotifier<String> list = ItemListNotifier<String>();
+      expect(list.items, isEmpty);
+
+      final Future<List<String>> addedStreamItems = list.onAdded.toList();
+      final Future<List<String>> removedStreamItems = list.onRemoved.toList();
+
+      list.updateWithNewList(<String>['aaa']);
+      list.updateWithNewList(<String>['aaa', 'bbb']);
+      list.updateWithNewList(<String>['bbb']);
+      list.dispose();
+
+      final List<String> addedItems = await addedStreamItems;
+      final List<String> removedItems = await removedStreamItems;
+
+      expect(addedItems.length, 2);
+      expect(addedItems.first, 'aaa');
+      expect(addedItems[1], 'bbb');
+
+      expect(removedItems.length, 1);
+      expect(removedItems.first, 'aaa');
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/bug_report_test.dart b/packages/flutter_tools/test/general.shard/bug_report_test.dart
new file mode 100644
index 0000000..4ee8d33
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/bug_report_test.dart
@@ -0,0 +1,35 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_testing/file_testing.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:flutter_tools/executable.dart' as tools;
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/os.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  Cache.disableLocking();
+
+  int exitCode;
+  setExitFunctionForTests((int code) {
+    exitCode = code;
+  });
+
+  group('--bug-report', () {
+    testUsingContext('generates valid zip file', () async {
+      await tools.main(<String>['devices', '--bug-report']);
+      expect(exitCode, 0);
+      verify(os.zip(any, argThat(hasPath(matches(r'bugreport_01\.zip')))));
+    }, overrides: <Type, Generator>{
+      OperatingSystemUtils: () => MockOperatingSystemUtils(),
+    });
+  });
+}
+
+class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { }
diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart
new file mode 100644
index 0000000..e4bc22a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_info_test.dart
@@ -0,0 +1,53 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'package:flutter_tools/src/build_info.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  setUpAll(() { });
+
+  group('Validate build number', () {
+    setUp(() async { });
+
+    testUsingContext('CFBundleVersion for iOS', () async {
+      String buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, 'xyz');
+      expect(buildName, '0');
+      buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, '123.xyz');
+      expect(buildName, '123');
+      buildName = validatedBuildNumberForPlatform(TargetPlatform.ios, '123.456.xyz');
+      expect(buildName, '123.456');
+    });
+
+    testUsingContext('versionCode for Android', () async {
+      String buildName = validatedBuildNumberForPlatform(TargetPlatform.android_arm, '123.abc+-');
+      expect(buildName, '123');
+      buildName = validatedBuildNumberForPlatform(TargetPlatform.android_arm, 'abc');
+      expect(buildName, '1');
+    });
+  });
+
+  group('Validate build name', () {
+    setUp(() async { });
+
+    testUsingContext('CFBundleShortVersionString for iOS', () async {
+      String buildName = validatedBuildNameForPlatform(TargetPlatform.ios, 'xyz');
+      expect(buildName, '0.0.0');
+      buildName = validatedBuildNameForPlatform(TargetPlatform.ios, '123.456.xyz');
+      expect(buildName, '123.456.0');
+      buildName = validatedBuildNameForPlatform(TargetPlatform.ios, '123.xyz');
+      expect(buildName, '123.0.0');
+    });
+
+    testUsingContext('versionName for Android', () async {
+      String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, '123.abc+-');
+      expect(buildName, '123.abc+-');
+      buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, 'abc+-');
+      expect(buildName, 'abc+-');
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/build_runner/multiroot_asset_reader_test.dart b/packages/flutter_tools/test/general.shard/build_runner/multiroot_asset_reader_test.dart
new file mode 100644
index 0000000..09656b4
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_runner/multiroot_asset_reader_test.dart
@@ -0,0 +1,62 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:build/build.dart';
+import 'package:build_runner_core/build_runner_core.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_runner/web_compilation_delegate.dart';
+
+import '../../src/common.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  group('MultirootFileBasedAssetReader', () {
+    Testbed testbed;
+    FakePackageGraph packageGraph;
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        final PackageNode root = PackageNode('foobar', fs.currentDirectory.path, DependencyType.path);
+        packageGraph = FakePackageGraph(root, <String, PackageNode>{'foobar': root});
+        fs.file(fs.path.join('lib', 'main.dart'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync('main');
+        fs.file(fs.path.join('.dart_tool', 'build', 'generated', 'foobar', 'lib', 'bar.dart'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync('bar');
+        fs.file('pubspec.yaml')
+          ..createSync()
+          ..writeAsStringSync('name: foobar');
+      });
+    });
+
+    test('Can find assets from the generated directory', () => testbed.run(() async {
+      final MultirootFileBasedAssetReader reader = MultirootFileBasedAssetReader(
+        packageGraph,
+        fs.directory(fs.path.join('.dart_tool', 'build', 'generated'))
+      );
+
+      // Note: we can't read from the regular directory because the default
+      // asset reader uses the regular file system.
+      expect(await reader.canRead(AssetId('foobar', 'lib/bar.dart')), true);
+      expect(await reader.readAsString(AssetId('foobar', 'lib/bar.dart')), 'bar');
+      expect(await reader.readAsBytes(AssetId('foobar', 'lib/bar.dart')), utf8.encode('bar'));
+    }));
+  });
+}
+
+class FakePackageGraph implements PackageGraph {
+  FakePackageGraph(this.root, this.allPackages);
+
+  @override
+  final Map<String, PackageNode> allPackages;
+
+  @override
+  final PackageNode root;
+
+  @override
+  PackageNode operator [](String packageName) => allPackages[packageName];
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
new file mode 100644
index 0000000..95ae999
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/build_system_test.dart
@@ -0,0 +1,554 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/exceptions.dart';
+import 'package:flutter_tools/src/build_system/file_hash_store.dart';
+import 'package:flutter_tools/src/build_system/filecache.pb.dart' as pb;
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/convert.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  group(Target, () {
+    Testbed testbed;
+    MockPlatform mockPlatform;
+    Environment environment;
+    Target fooTarget;
+    Target barTarget;
+    Target fizzTarget;
+    BuildSystem buildSystem;
+    int fooInvocations;
+    int barInvocations;
+
+    setUp(() {
+      fooInvocations = 0;
+      barInvocations = 0;
+      mockPlatform = MockPlatform();
+      // Keep file paths the same.
+      when(mockPlatform.isWindows).thenReturn(false);
+      testbed = Testbed(
+        setup: () {
+          environment = Environment(
+            projectDir: fs.currentDirectory,
+          );
+          fs.file('foo.dart').createSync(recursive: true);
+          fs.file('pubspec.yaml').createSync();
+          fooTarget = Target(
+            name: 'foo',
+            inputs: const <Source>[
+              Source.pattern('{PROJECT_DIR}/foo.dart'),
+            ],
+            outputs: const <Source>[
+              Source.pattern('{BUILD_DIR}/out'),
+            ],
+            dependencies: <Target>[],
+            buildAction: (Map<String, ChangeType> updates, Environment environment) {
+              environment
+                .buildDir
+                .childFile('out')
+                ..createSync(recursive: true)
+                ..writeAsStringSync('hey');
+              fooInvocations++;
+            }
+          );
+          barTarget = Target(
+            name: 'bar',
+            inputs: const <Source>[
+              Source.pattern('{BUILD_DIR}/out'),
+            ],
+            outputs: const <Source>[
+              Source.pattern('{BUILD_DIR}/bar'),
+            ],
+            dependencies: <Target>[fooTarget],
+            buildAction: (Map<String, ChangeType> updates, Environment environment) {
+              environment.buildDir
+                .childFile('bar')
+                ..createSync(recursive: true)
+                ..writeAsStringSync('there');
+              barInvocations++;
+            }
+          );
+          fizzTarget = Target(
+              name: 'fizz',
+              inputs: const <Source>[
+                Source.pattern('{BUILD_DIR}/out'),
+              ],
+              outputs: const <Source>[
+                Source.pattern('{BUILD_DIR}/fizz'),
+              ],
+              dependencies: <Target>[fooTarget],
+              buildAction: (Map<String, ChangeType> updates, Environment environment) {
+                throw Exception('something bad happens');
+              }
+          );
+          buildSystem = BuildSystem(<String, Target>{
+            fooTarget.name: fooTarget,
+            barTarget.name: barTarget,
+            fizzTarget.name: fizzTarget,
+          });
+        },
+        overrides: <Type, Generator>{
+          Platform: () => mockPlatform,
+        }
+      );
+    });
+
+    test('can describe build rules', () => testbed.run(() {
+      expect(buildSystem.describe('foo', environment), <Object>[
+        <String, Object>{
+          'name': 'foo',
+          'dependencies': <String>[],
+          'inputs': <String>['/foo.dart'],
+          'outputs': <String>[fs.path.join(environment.buildDir.path, 'out')],
+          'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
+        }
+      ]);
+    }));
+
+    test('Throws exception if asked to build non-existent target', () => testbed.run(() {
+      expect(buildSystem.build('not_real', environment, const BuildSystemConfig()), throwsA(isInstanceOf<Exception>()));
+    }));
+
+    test('Throws exception if asked to build with missing inputs', () => testbed.run(() async {
+      // Delete required input file.
+      fs.file('foo.dart').deleteSync();
+      final BuildResult buildResult = await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      expect(buildResult.hasException, true);
+      expect(buildResult.exceptions.values.single.exception, isInstanceOf<MissingInputException>());
+    }));
+
+    test('Throws exception if it does not produce a specified output', () => testbed.run(() async {
+      final Target badTarget = Target
+        (buildAction: (Map<String, ChangeType> inputs, Environment environment) {},
+        inputs: const <Source>[
+          Source.pattern('{PROJECT_DIR}/foo.dart'),
+        ],
+        outputs: const <Source>[
+          Source.pattern('{BUILD_DIR}/out')
+        ],
+        name: 'bad'
+      );
+      buildSystem = BuildSystem(<String, Target>{
+        badTarget.name: badTarget,
+      });
+      final BuildResult result = await buildSystem.build('bad', environment, const BuildSystemConfig());
+
+      expect(result.hasException, true);
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingOutputException>());
+    }));
+
+    test('Saves a stamp file with inputs and outputs', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      final File stampFile = fs.file(fs.path.join(environment.buildDir.path, 'foo.stamp'));
+      expect(stampFile.existsSync(), true);
+
+      final Map<String, Object> stampContents = json.decode(stampFile.readAsStringSync());
+      expect(stampContents['inputs'], <Object>['/foo.dart']);
+    }));
+
+    test('Does not re-invoke build if stamp is valid', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      expect(fooInvocations, 1);
+    }));
+
+    test('Re-invoke build if input is modified', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      fs.file('foo.dart').writeAsStringSync('new contents');
+
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      expect(fooInvocations, 2);
+    }));
+
+    test('does not re-invoke build if input timestamp changes', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      fs.file('foo.dart').writeAsStringSync('');
+
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      expect(fooInvocations, 1);
+    }));
+
+    test('does not re-invoke build if output timestamp changes', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      environment.buildDir.childFile('out').writeAsStringSync('hey');
+
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      expect(fooInvocations, 1);
+    }));
+
+
+    test('Re-invoke build if output is modified', () => testbed.run(() async {
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+
+      environment.buildDir.childFile('out').writeAsStringSync('Something different');
+
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      expect(fooInvocations, 2);
+    }));
+
+    test('Runs dependencies of targets', () => testbed.run(() async {
+      await buildSystem.build('bar', environment, const BuildSystemConfig());
+
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'bar')).existsSync(), true);
+      expect(fooInvocations, 1);
+      expect(barInvocations, 1);
+    }));
+
+    test('handles a throwing build action', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('fizz', environment, const BuildSystemConfig());
+
+      expect(result.hasException, true);
+    }));
+
+    test('Can describe itself with JSON output', () => testbed.run(() {
+      environment.buildDir.createSync(recursive: true);
+      expect(fooTarget.toJson(environment), <String, dynamic>{
+        'inputs':  <Object>[
+          '/foo.dart'
+        ],
+        'outputs': <Object>[
+          fs.path.join(environment.buildDir.path, 'out'),
+        ],
+        'dependencies': <Object>[],
+        'name':  'foo',
+        'stamp': fs.path.join(environment.buildDir.path, 'foo.stamp'),
+      });
+    }));
+
+    test('Compute update recognizes added files', () => testbed.run(() async {
+      fs.directory('build').createSync();
+      final FileHashStore fileCache = FileHashStore(environment);
+      fileCache.initialize();
+      final List<File> inputs = fooTarget.resolveInputs(environment);
+      final Map<String, ChangeType> changes = await fooTarget.computeChanges(inputs, environment, fileCache);
+      fileCache.persist();
+
+      expect(changes, <String, ChangeType>{
+        '/foo.dart': ChangeType.Added
+      });
+
+      await buildSystem.build('foo', environment, const BuildSystemConfig());
+      final Map<String, ChangeType> secondChanges = await fooTarget.computeChanges(inputs, environment, fileCache);
+
+      expect(secondChanges, <String, ChangeType>{});
+    }));
+  });
+
+  group('FileCache', () {
+    Testbed testbed;
+    Environment environment;
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        fs.directory('build').createSync();
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+        );
+      });
+    });
+
+    test('Initializes file cache', () => testbed.run(() {
+      final FileHashStore fileCache = FileHashStore(environment);
+      fileCache.initialize();
+      fileCache.persist();
+
+      expect(fs.file(fs.path.join('build', '.filecache')).existsSync(), true);
+
+      final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
+      final pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
+
+      expect(fileStorage.files, isEmpty);
+      expect(fileStorage.version, 1);
+    }));
+
+    test('saves and restores to file cache', () => testbed.run(() {
+      final File file = fs.file('foo.dart')
+        ..createSync()
+        ..writeAsStringSync('hello');
+      final FileHashStore fileCache = FileHashStore(environment);
+      fileCache.initialize();
+      fileCache.hashFiles(<File>[file]);
+      fileCache.persist();
+      final String currentHash =  fileCache.currentHashes[file.resolveSymbolicLinksSync()];
+      final List<int> buffer = fs.file(fs.path.join('build', '.filecache')).readAsBytesSync();
+      pb.FileStorage fileStorage = pb.FileStorage.fromBuffer(buffer);
+
+      expect(fileStorage.files.single.hash, currentHash);
+      expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
+
+
+      final FileHashStore newFileCache = FileHashStore(environment);
+      newFileCache.initialize();
+      expect(newFileCache.currentHashes, isEmpty);
+      expect(newFileCache.previousHashes[fs.path.absolute('foo.dart')],  currentHash);
+      newFileCache.persist();
+
+      // Still persisted correctly.
+      fileStorage = pb.FileStorage.fromBuffer(buffer);
+
+      expect(fileStorage.files.single.hash, currentHash);
+      expect(fileStorage.files.single.path, file.resolveSymbolicLinksSync());
+    }));
+  });
+
+  group('Target', () {
+    Testbed testbed;
+    MockPlatform mockPlatform;
+    Environment environment;
+    Target sharedTarget;
+    BuildSystem buildSystem;
+    int shared;
+
+    setUp(() {
+      shared = 0;
+      Cache.flutterRoot = '';
+      mockPlatform = MockPlatform();
+      // Keep file paths the same.
+      when(mockPlatform.isWindows).thenReturn(false);
+      when(mockPlatform.isLinux).thenReturn(true);
+      when(mockPlatform.isMacOS).thenReturn(false);
+      testbed = Testbed(
+          setup: () {
+            environment = Environment(
+              projectDir: fs.currentDirectory,
+            );
+            fs.file('foo.dart').createSync(recursive: true);
+            fs.file('pubspec.yaml').createSync();
+            sharedTarget = Target(
+              name: 'shared',
+              inputs: const <Source>[
+                Source.pattern('{PROJECT_DIR}/foo.dart'),
+              ],
+              outputs: const <Source>[],
+              dependencies: <Target>[],
+              buildAction: (Map<String, ChangeType> updates, Environment environment) {
+                shared += 1;
+              }
+            );
+            final Target fooTarget = Target(
+                name: 'foo',
+                inputs: const <Source>[
+                  Source.pattern('{PROJECT_DIR}/foo.dart'),
+                ],
+                outputs: const <Source>[
+                  Source.pattern('{BUILD_DIR}/out'),
+                ],
+                dependencies: <Target>[sharedTarget],
+                buildAction: (Map<String, ChangeType> updates, Environment environment) {
+                  environment
+                    .buildDir
+                    .childFile('out')
+                    ..createSync(recursive: true)
+                    ..writeAsStringSync('hey');
+                }
+            );
+            final Target barTarget = Target(
+                name: 'bar',
+                inputs: const <Source>[
+                  Source.pattern('{BUILD_DIR}/out'),
+                ],
+                outputs: const <Source>[
+                  Source.pattern('{BUILD_DIR}/bar'),
+                ],
+                dependencies: <Target>[fooTarget, sharedTarget],
+                buildAction: (Map<String, ChangeType> updates, Environment environment) {
+                  environment
+                    .buildDir
+                    .childFile('bar')
+                    ..createSync(recursive: true)
+                    ..writeAsStringSync('there');
+                }
+            );
+            buildSystem = BuildSystem(<String, Target>{
+              fooTarget.name: fooTarget,
+              barTarget.name: barTarget,
+              sharedTarget.name: sharedTarget,
+            });
+          },
+          overrides: <Type, Generator>{
+            Platform: () => mockPlatform,
+          }
+      );
+    });
+
+    test('Only invokes shared target once', () => testbed.run(() async {
+      await buildSystem.build('bar', environment, const BuildSystemConfig());
+
+      expect(shared, 1);
+    }));
+  });
+
+  group('Source', () {
+    Testbed testbed;
+    SourceVisitor visitor;
+    Environment environment;
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        fs.directory('cache').createSync();
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+          buildDir: fs.directory('build'),
+        );
+        visitor = SourceVisitor(environment);
+        environment.buildDir.createSync(recursive: true);
+      });
+    });
+
+    test('configures implicit vs explict correctly', () => testbed.run(() {
+      expect(const Source.pattern('{PROJECT_DIR}/foo').implicit, false);
+      expect(const Source.pattern('{PROJECT_DIR}/*foo').implicit, true);
+      expect(Source.function((Environment environment) => <File>[]).implicit, true);
+      expect(Source.behavior(TestBehavior()).implicit, true);
+    }));
+
+    test('can substitute {PROJECT_DIR}/foo', () => testbed.run(() {
+      fs.file('foo').createSync();
+      const Source fooSource = Source.pattern('{PROJECT_DIR}/foo');
+      fooSource.accept(visitor);
+
+      expect(visitor.sources.single.path, fs.path.absolute('foo'));
+    }));
+
+    test('can substitute {BUILD_DIR}/bar', () => testbed.run(() {
+      final String path = fs.path.join(environment.buildDir.path, 'bar');
+      fs.file(path).createSync();
+      const Source barSource = Source.pattern('{BUILD_DIR}/bar');
+      barSource.accept(visitor);
+
+      expect(visitor.sources.single.path, fs.path.absolute(path));
+    }));
+
+    test('can substitute Artifact', () => testbed.run(() {
+      final String path = fs.path.join(
+        Cache.instance.getArtifactDirectory('engine').path,
+        'windows-x64',
+        'foo',
+      );
+      fs.file(path).createSync(recursive: true);
+      const Source fizzSource = Source.artifact(Artifact.windowsDesktopPath, platform: TargetPlatform.windows_x64);
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources.single.resolveSymbolicLinksSync(), fs.path.absolute(path));
+    }));
+
+    test('can substitute {PROJECT_DIR}/*.fizz', () => testbed.run(() {
+      const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.fizz');
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources, isEmpty);
+
+      fs.file('foo.fizz').createSync();
+      fs.file('foofizz').createSync();
+
+
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources.single.path, fs.path.absolute('foo.fizz'));
+    }));
+
+    test('can substitute {PROJECT_DIR}/fizz.*', () => testbed.run(() {
+      const Source fizzSource = Source.pattern('{PROJECT_DIR}/fizz.*');
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources, isEmpty);
+
+      fs.file('fizz.foo').createSync();
+      fs.file('fizz').createSync();
+
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources.single.path, fs.path.absolute('fizz.foo'));
+    }));
+
+
+    test('can substitute {PROJECT_DIR}/a*bc', () => testbed.run(() {
+      const Source fizzSource = Source.pattern('{PROJECT_DIR}/bc*bc');
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources, isEmpty);
+
+      fs.file('bcbc').createSync();
+      fs.file('bc').createSync();
+
+      fizzSource.accept(visitor);
+
+      expect(visitor.sources.single.path, fs.path.absolute('bcbc'));
+    }));
+
+
+    test('crashes on bad substitute of two **', () => testbed.run(() {
+      const Source fizzSource = Source.pattern('{PROJECT_DIR}/*.*bar');
+
+      fs.file('abcd.bar').createSync();
+
+      expect(() => fizzSource.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
+    }));
+
+
+    test('can\'t substitute foo', () => testbed.run(() {
+      const Source invalidBase = Source.pattern('foo');
+
+      expect(() => invalidBase.accept(visitor), throwsA(isInstanceOf<InvalidPatternException>()));
+    }));
+  });
+
+
+
+  test('Can find dependency cycles', () {
+    final Target barTarget = Target(
+      name: 'bar',
+      inputs: <Source>[],
+      outputs: <Source>[],
+      buildAction: null,
+      dependencies: nonconst(<Target>[])
+    );
+    final Target fooTarget = Target(
+      name: 'foo',
+      inputs: <Source>[],
+      outputs: <Source>[],
+      buildAction: null,
+      dependencies: nonconst(<Target>[])
+    );
+    barTarget.dependencies.add(fooTarget);
+    fooTarget.dependencies.add(barTarget);
+    expect(() => checkCycles(barTarget), throwsA(isInstanceOf<CycleException>()));
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
+
+// Work-around for silly lint check.
+T nonconst<T>(T input) => input;
+
+class TestBehavior extends SourceBehavior {
+  @override
+  List<File> inputs(Environment environment) {
+    return null;
+  }
+
+  @override
+  List<File> outputs(Environment environment) {
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/exceptions_test.dart b/packages/flutter_tools/test/general.shard/build_system/exceptions_test.dart
new file mode 100644
index 0000000..18de1fd
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/exceptions_test.dart
@@ -0,0 +1,72 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/exceptions.dart';
+
+import '../../src/common.dart';
+
+void main() {
+  test('Exceptions', () {
+    final MissingInputException missingInputException = MissingInputException(
+        <File>[fs.file('foo'), fs.file('bar')], 'example');
+    final CycleException cycleException = CycleException(const <Target>{
+      Target(
+        name: 'foo',
+        buildAction: null,
+        inputs: <Source>[],
+        outputs: <Source>[],
+      ),
+      Target(
+        name: 'bar',
+        buildAction: null,
+        inputs: <Source>[],
+        outputs: <Source>[],
+      )
+    });
+    final InvalidPatternException invalidPatternException = InvalidPatternException(
+      'ABC'
+    );
+    final MissingOutputException missingOutputException = MissingOutputException(
+      <File>[ fs.file('foo'), fs.file('bar') ],
+      'example'
+    );
+    final MisplacedOutputException misplacedOutputException = MisplacedOutputException(
+      'foo',
+      'example',
+    );
+    final MissingDefineException missingDefineException = MissingDefineException(
+      'foobar',
+      'example',
+    );
+
+    expect(
+        missingInputException.toString(),
+        'foo, bar were declared as an inputs, '
+        'but did not exist. Check the definition of target:example for errors');
+    expect(
+        cycleException.toString(),
+        'Dependency cycle detected in build: foo -> bar'
+    );
+    expect(
+        invalidPatternException.toString(),
+        'The pattern "ABC" is not valid'
+    );
+    expect(
+        missingOutputException.toString(),
+        'foo, bar were declared as outputs, but were not generated by the '
+        'action. Check the definition of target:example for errors'
+    );
+    expect(
+        misplacedOutputException.toString(),
+        'Target example produced an output at foo which is outside of the '
+        'current build or project directory',
+    );
+    expect(
+        missingDefineException.toString(),
+        'Target example required define foobar but it was not provided'
+    );
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart
new file mode 100644
index 0000000..a5b2034
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/assets_test.dart
@@ -0,0 +1,70 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/targets/assets.dart';
+
+import '../../../src/common.dart';
+import '../../../src/testbed.dart';
+
+void main() {
+  group('copy_assets', () {
+    Testbed testbed;
+    BuildSystem buildSystem;
+    Environment environment;
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+        );
+        buildSystem = BuildSystem(<String, Target>{
+          copyAssets.name: copyAssets,
+        });
+        fs.file(fs.path.join('assets', 'foo', 'bar.png'))
+          ..createSync(recursive: true);
+        fs.file('.packages')
+          ..createSync();
+        fs.file('pubspec.yaml')
+          ..createSync()
+          ..writeAsStringSync('''
+name: example
+
+flutter:
+  assets:
+    - assets/foo/bar.png
+''');
+      });
+    });
+
+    test('Copies files to correct asset directory', () => testbed.run(() async {
+      await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
+
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'AssetManifest.json')).existsSync(), true);
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'FontManifest.json')).existsSync(), true);
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'LICENSE')).existsSync(), true);
+      // See https://github.com/flutter/flutter/issues/35293
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true);
+    }));
+
+    test('Does not leave stale files in build directory', () => testbed.run(() async {
+      await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
+
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), true);
+      // Modify manifest to remove asset.
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync('''
+name: example
+
+flutter:
+''');
+      await buildSystem.build('copy_assets', environment, const BuildSystemConfig());
+
+      // See https://github.com/flutter/flutter/issues/35293
+      expect(fs.file(fs.path.join(environment.buildDir.path, 'flutter_assets', 'assets/foo/bar.png')).existsSync(), false);
+    }));
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
new file mode 100644
index 0000000..585ac48
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart
@@ -0,0 +1,224 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/build.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/exceptions.dart';
+import 'package:flutter_tools/src/build_system/targets/dart.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/compile.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../../src/common.dart';
+import '../../../src/mocks.dart';
+import '../../../src/testbed.dart';
+
+void main() {
+  group('dart rules', () {
+    Testbed testbed;
+    BuildSystem buildSystem;
+    Environment androidEnvironment;
+    Environment iosEnvironment;
+    MockProcessManager mockProcessManager;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      testbed = Testbed(setup: () {
+        androidEnvironment = Environment(
+          projectDir: fs.currentDirectory,
+          defines: <String, String>{
+            kBuildMode: getNameForBuildMode(BuildMode.profile),
+            kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
+          }
+        );
+        iosEnvironment = Environment(
+          projectDir: fs.currentDirectory,
+          defines: <String, String>{
+            kBuildMode: getNameForBuildMode(BuildMode.profile),
+            kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
+          }
+        );
+        buildSystem = BuildSystem();
+        HostPlatform hostPlatform;
+        if (platform.isWindows) {
+          hostPlatform = HostPlatform.windows_x64;
+        } else if (platform.isLinux) {
+          hostPlatform = HostPlatform.linux_x64;
+        } else if (platform.isMacOS) {
+           hostPlatform = HostPlatform.darwin_x64;
+        } else {
+          assert(false);
+        }
+         final String skyEngineLine = platform.isWindows
+            ? r'sky_engine:file:///C:/bin/cache/pkg/sky_engine/lib/'
+            : 'sky_engine:file:///bin/cache/pkg/sky_engine/lib/';
+        fs.file('.packages')
+          ..createSync()
+          ..writeAsStringSync('''
+# Generated
+$skyEngineLine
+flutter_tools:lib/''');
+        final String engineArtifacts = fs.path.join('bin', 'cache',
+            'artifacts', 'engine');
+        final List<String> paths = <String>[
+          fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui',
+            'ui.dart'),
+          fs.path.join('bin', 'cache', 'pkg', 'sky_engine', 'sdk_ext',
+              'vmservice_io.dart'),
+          fs.path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart'),
+          fs.path.join(engineArtifacts, getNameForHostPlatform(hostPlatform),
+              'frontend_server.dart.snapshot'),
+          fs.path.join(engineArtifacts, 'android-arm-profile',
+              getNameForHostPlatform(hostPlatform), 'gen_snapshot'),
+          fs.path.join(engineArtifacts, 'ios-profile', 'gen_snapshot'),
+          fs.path.join(engineArtifacts, 'common', 'flutter_patched_sdk',
+              'platform_strong.dill'),
+          fs.path.join('lib', 'foo.dart'),
+          fs.path.join('lib', 'bar.dart'),
+          fs.path.join('lib', 'fizz'),
+        ];
+        for (String path in paths) {
+          fs.file(path).createSync(recursive: true);
+        }
+      }, overrides: <Type, Generator>{
+        KernelCompilerFactory: () => FakeKernelCompilerFactory(),
+        GenSnapshot: () => FakeGenSnapshot(),
+      });
+    });
+
+    test('kernel_snapshot Produces correct output directory', () => testbed.run(() async {
+      await buildSystem.build('kernel_snapshot', androidEnvironment, const BuildSystemConfig());
+
+      expect(fs.file(fs.path.join(androidEnvironment.buildDir.path,'main.app.dill')).existsSync(), true);
+    }));
+
+    test('kernel_snapshot throws error if missing build mode', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('kernel_snapshot',
+          androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
+    }));
+
+    test('aot_elf_profile Produces correct output directory', () => testbed.run(() async {
+      await buildSystem.build('aot_elf_profile', androidEnvironment, const BuildSystemConfig());
+
+      expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'main.app.dill')).existsSync(), true);
+      expect(fs.file(fs.path.join(androidEnvironment.buildDir.path, 'app.so')).existsSync(), true);
+    }));
+
+    test('aot_elf_profile throws error if missing build mode', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('aot_elf_profile',
+          androidEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
+    }));
+
+
+    test('aot_elf_profile throws error if missing target platform', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('aot_elf_profile',
+          androidEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
+    }));
+
+
+    test('aot_assembly_profile throws error if missing build mode', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('aot_assembly_profile',
+          iosEnvironment..defines.remove(kBuildMode), const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
+    }));
+
+    test('aot_assembly_profile throws error if missing target platform', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('aot_assembly_profile',
+          iosEnvironment..defines.remove(kTargetPlatform), const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<MissingDefineException>());
+    }));
+
+    test('aot_assembly_profile throws error if built for non-iOS platform', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('aot_assembly_profile',
+          androidEnvironment, const BuildSystemConfig());
+
+      expect(result.exceptions.values.single.exception, isInstanceOf<Exception>());
+    }));
+
+    test('aot_assembly_profile will lipo binaries together when multiple archs are requested', () => testbed.run(() async {
+      iosEnvironment.defines[kIosArchs] ='armv7,arm64';
+      when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+        fs.file(fs.path.join(iosEnvironment.buildDir.path, 'App.framework', 'App'))
+            .createSync(recursive: true);
+        return FakeProcessResult(
+          stdout: '',
+          stderr: '',
+        );
+      });
+      final BuildResult result = await buildSystem.build('aot_assembly_profile',
+          iosEnvironment, const BuildSystemConfig());
+
+      expect(result.success, true);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    }));
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class FakeGenSnapshot implements GenSnapshot {
+  @override
+  Future<int> run({SnapshotType snapshotType, IOSArch iosArch, Iterable<String> additionalArgs = const <String>[]}) async {
+    final Directory out = fs.file(additionalArgs.last).parent;
+    if (iosArch == null) {
+      out.childFile('app.so').createSync();
+      out.childFile('gen_snapshot.d').createSync();
+      return 0;
+    }
+    out.childDirectory('App.framework').childFile('App').createSync(recursive: true);
+    out.childFile('snapshot_assembly.S').createSync();
+    out.childFile('snapshot_assembly.o').createSync();
+    return 0;
+  }
+}
+
+class FakeKernelCompilerFactory implements KernelCompilerFactory {
+  FakeKernelCompiler kernelCompiler = FakeKernelCompiler();
+
+  @override
+  Future<KernelCompiler> create(FlutterProject flutterProject) async {
+    return kernelCompiler;
+  }
+}
+
+class FakeKernelCompiler implements KernelCompiler {
+  @override
+  Future<CompilerOutput> compile({
+    String sdkRoot,
+    String mainPath,
+    String outputFilePath,
+    String depFilePath,
+    TargetModel targetModel = TargetModel.flutter,
+    bool linkPlatformKernelIn = false,
+    bool aot = false,
+    bool trackWidgetCreation,
+    List<String> extraFrontEndOptions,
+    String incrementalCompilerByteStorePath,
+    String packagesPath,
+    List<String> fileSystemRoots,
+    String fileSystemScheme,
+    bool targetProductVm = false,
+    String initializeFromDill}) async {
+      fs.file(outputFilePath).createSync(recursive: true);
+      return CompilerOutput(outputFilePath, 0, null);
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/linux_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/linux_test.dart
new file mode 100644
index 0000000..801f12a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/linux_test.dart
@@ -0,0 +1,84 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/targets/linux.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../src/common.dart';
+import '../../../src/testbed.dart';
+
+void main() {
+  group('unpack_linux', () {
+    Testbed testbed;
+    BuildSystem buildSystem;
+    Environment environment;
+    MockPlatform mockPlatform;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      mockPlatform = MockPlatform();
+      when(mockPlatform.isWindows).thenReturn(false);
+      when(mockPlatform.isMacOS).thenReturn(false);
+      when(mockPlatform.isLinux).thenReturn(true);
+      testbed = Testbed(setup: () {
+        Cache.flutterRoot = '';
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+        );
+        buildSystem = BuildSystem(<String, Target>{
+          unpackLinux.name: unpackLinux,
+        });
+        fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').createSync(recursive: true);
+        fs.file('bin/cache/artifacts/engine/linux-x64/flutter_export.h').createSync();
+        fs.file('bin/cache/artifacts/engine/linux-x64/flutter_messenger.h').createSync();
+        fs.file('bin/cache/artifacts/engine/linux-x64/flutter_plugin_registrar.h').createSync();
+        fs.file('bin/cache/artifacts/engine/linux-x64/flutter_glfw.h').createSync();
+        fs.file('bin/cache/artifacts/engine/linux-x64/icudtl.dat').createSync();
+        fs.file('bin/cache/artifacts/engine/linux-x64/cpp_client_wrapper/foo').createSync(recursive: true);
+        fs.directory('linux').createSync();
+      }, overrides: <Type, Generator>{
+        Platform: () => mockPlatform,
+      });
+    });
+
+    test('Copies files to correct cache directory', () => testbed.run(() async {
+      final BuildResult result = await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
+
+      expect(result.hasException, false);
+      expect(fs.file('linux/flutter/libflutter_linux.so').existsSync(), true);
+      expect(fs.file('linux/flutter/flutter_export.h').existsSync(), true);
+      expect(fs.file('linux/flutter/flutter_messenger.h').existsSync(), true);
+      expect(fs.file('linux/flutter/flutter_plugin_registrar.h').existsSync(), true);
+      expect(fs.file('linux/flutter/flutter_glfw.h').existsSync(), true);
+      expect(fs.file('linux/flutter/icudtl.dat').existsSync(), true);
+      expect(fs.file('linux/flutter/cpp_client_wrapper/foo').existsSync(), true);
+    }));
+
+    test('Does not re-copy files unecessarily', () => testbed.run(() async {
+      await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
+      final DateTime modified = fs.file('linux/flutter/libflutter_linux.so').statSync().modified;
+      await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
+
+      expect(fs.file('linux/flutter/libflutter_linux.so').statSync().modified, equals(modified));
+    }));
+
+    test('Detects changes in input cache files', () => testbed.run(() async {
+      await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
+      fs.file('bin/cache/artifacts/engine/linux-x64/libflutter_linux.so').writeAsStringSync('asd'); // modify cache.
+
+      await buildSystem.build('unpack_linux', environment, const BuildSystemConfig());
+
+      expect(fs.file('linux/flutter/libflutter_linux.so').readAsStringSync(), 'asd');
+    }));
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
new file mode 100644
index 0000000..6d656b3
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart
@@ -0,0 +1,115 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/process_manager.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/targets/macos.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../../src/common.dart';
+import '../../../src/testbed.dart';
+
+void main() {
+  group('unpack_macos', () {
+    Testbed testbed;
+    BuildSystem buildSystem;
+    Environment environment;
+    MockPlatform mockPlatform;
+
+    setUp(() {
+      mockPlatform = MockPlatform();
+      when(mockPlatform.isWindows).thenReturn(false);
+      when(mockPlatform.isMacOS).thenReturn(true);
+      when(mockPlatform.isLinux).thenReturn(false);
+      testbed = Testbed(setup: () {
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+        );
+        buildSystem = BuildSystem(<String, Target>{
+          unpackMacos.name: unpackMacos,
+        });
+        final List<File> inputs = <File>[
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/FlutterMacOS'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEReshapeListener.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEView.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FLEViewController.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterChannels.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterCodecs.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterMacOS.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Modules/module.modulemap'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/icudtl.dat'),
+          fs.file('bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework/Resources/info.plist'),
+        ];
+        for (File input in inputs) {
+          input.createSync(recursive: true);
+        }
+        when(processManager.runSync(any)).thenAnswer((Invocation invocation) {
+          final List<String> arguments = invocation.positionalArguments.first;
+          final Directory source = fs.directory(arguments[arguments.length - 2]);
+          final Directory target = fs.directory(arguments.last)
+            ..createSync(recursive: true);
+          for (FileSystemEntity entity in source.listSync(recursive: true)) {
+            if (entity is File) {
+              final String relative = fs.path.relative(entity.path, from: source.path);
+              final String destination = fs.path.join(target.path, relative);
+              if (!fs.file(destination).parent.existsSync()) {
+                fs.file(destination).parent.createSync();
+              }
+              entity.copySync(destination);
+            }
+          }
+          return FakeProcessResult()..exitCode = 0;
+        });
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => MockProcessManager(),
+        Platform: () => mockPlatform,
+      });
+    });
+
+    test('Copies files to correct cache directory', () => testbed.run(() async {
+      await buildSystem.build('unpack_macos', environment, const BuildSystemConfig());
+
+      expect(fs.directory('macos/Flutter/FlutterMacOS.framework').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/FlutterMacOS').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEOpenGLContextHandling.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEReshapeListener.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEView.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FLEViewController.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterBinaryMessenger.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterChannels.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterCodecs.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterMacOS.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginMacOS.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Headers/FlutterPluginRegisrarMacOS.h').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Modules/module.modulemap').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/icudtl.dat').existsSync(), true);
+      expect(fs.file('macos/Flutter/FlutterMacOS.framework/Resources/info.plist').existsSync(), true);
+    }));
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class FakeProcessResult implements ProcessResult {
+  @override
+  int exitCode;
+
+  @override
+  int pid = 0;
+
+  @override
+  String stderr = '';
+
+  @override
+  String stdout = '';
+}
diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/windows_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/windows_test.dart
new file mode 100644
index 0000000..c167f4c
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/build_system/targets/windows_test.dart
@@ -0,0 +1,97 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/build_system/targets/windows.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../../src/common.dart';
+import '../../../src/testbed.dart';
+
+void main() {
+  group('unpack_windows', () {
+    Testbed testbed;
+    BuildSystem buildSystem;
+    Environment environment;
+    Platform platform;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      Cache.flutterRoot = '';
+      platform = MockPlatform();
+      when(platform.isWindows).thenReturn(true);
+      when(platform.isMacOS).thenReturn(false);
+      when(platform.isLinux).thenReturn(false);
+      when(platform.pathSeparator).thenReturn(r'\');
+      testbed = Testbed(setup: () {
+        environment = Environment(
+          projectDir: fs.currentDirectory,
+        );
+        buildSystem = BuildSystem(<String, Target>{
+          unpackWindows.name: unpackWindows,
+        });
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').createSync(recursive: true);
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.exp').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.lib').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_windows.dll.pdb').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\lutter_export.h').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_messenger.h').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_plugin_registrar.h').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_glfw.h').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\icudtl.dat').createSync();
+        fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\cpp_client_wrapper\foo').createSync(recursive: true);
+        fs.directory('windows').createSync();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => MemoryFileSystem(style: FileSystemStyle.windows),
+        Platform: () => platform,
+      });
+    });
+
+    test('Copies files to correct cache directory', () => testbed.run(() async {
+      await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
+
+      expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_windows.dll').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.exp').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.lib').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_windows.dll.pdb').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_export.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_messenger.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_plugin_registrar.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\flutter_glfw.h').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\icudtl.dat').existsSync(), true);
+      expect(fs.file(r'C:\windows\flutter\cpp_client_wrapper\foo').existsSync(), true);
+    }));
+
+    test('Does not re-copy files unecessarily', () => testbed.run(() async {
+      await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
+      final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified;
+      await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
+
+      expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, equals(modified));
+    }));
+
+    test('Detects changes in input cache files', () => testbed.run(() async {
+      await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
+      final DateTime modified = fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified;
+      fs.file(r'C:\bin\cache\artifacts\engine\windows-x64\flutter_export.h').writeAsStringSync('asd'); // modify cache.
+
+      await buildSystem.build('unpack_windows', environment, const BuildSystemConfig());
+
+      expect(fs.file(r'C:\windows\flutter\flutter_export.h').statSync().modified, isNot(modified));
+    }));
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
new file mode 100644
index 0000000..18b97b6
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -0,0 +1,165 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+import 'package:file/memory.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/base/io.dart' show InternetAddress, SocketException;
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('$Cache.checkLockAcquired', () {
+    setUp(() {
+      Cache.enableLocking();
+    });
+
+    tearDown(() {
+      // Restore locking to prevent potential side-effects in
+      // tests outside this group (this option is globally shared).
+      Cache.enableLocking();
+    });
+
+    test('should throw when locking is not acquired', () {
+      expect(() => Cache.checkLockAcquired(), throwsStateError);
+    });
+
+    test('should not throw when locking is disabled', () {
+      Cache.disableLocking();
+      Cache.checkLockAcquired();
+    });
+
+    testUsingContext('should not throw when lock is acquired', () async {
+      await Cache.lock();
+      Cache.checkLockAcquired();
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MockFileSystem(),
+    });
+
+    testUsingContext('should not throw when FLUTTER_ALREADY_LOCKED is set', () async {
+      Cache.checkLockAcquired();
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()..environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'},
+    });
+  });
+
+  group('Cache', () {
+    final MockCache mockCache = MockCache();
+    final MemoryFileSystem fs = MemoryFileSystem();
+
+    testUsingContext('Gradle wrapper should not be up to date, if some cached artifact is not available', () {
+      final GradleWrapper gradleWrapper = GradleWrapper(mockCache);
+      final Directory directory = fs.directory('/Applications/flutter/bin/cache');
+      directory.createSync(recursive: true);
+      fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradle', 'wrapper', 'gradle-wrapper.jar')).createSync(recursive: true);
+      when(mockCache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'))).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
+      expect(gradleWrapper.isUpToDateInner(), false);
+    }, overrides: <Type, Generator>{
+      Cache: ()=> mockCache,
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('Gradle wrapper should be up to date, only if all cached artifact are available', () {
+      final GradleWrapper gradleWrapper = GradleWrapper(mockCache);
+      final Directory directory = fs.directory('/Applications/flutter/bin/cache');
+      directory.createSync(recursive: true);
+      fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradle', 'wrapper', 'gradle-wrapper.jar')).createSync(recursive: true);
+      fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradlew')).createSync(recursive: true);
+      fs.file(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper', 'gradlew.bat')).createSync(recursive: true);
+
+      when(mockCache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'))).thenReturn(fs.directory(fs.path.join(directory.path, 'artifacts', 'gradle_wrapper')));
+      expect(gradleWrapper.isUpToDateInner(), true);
+    }, overrides: <Type, Generator>{
+      Cache: ()=> mockCache,
+      FileSystem: () => fs,
+    });
+
+    test('should not be up to date, if some cached artifact is not', () {
+      final CachedArtifact artifact1 = MockCachedArtifact();
+      final CachedArtifact artifact2 = MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(false);
+      final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      expect(cache.isUpToDate(), isFalse);
+    });
+    test('should be up to date, if all cached artifacts are', () {
+      final CachedArtifact artifact1 = MockCachedArtifact();
+      final CachedArtifact artifact2 = MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(true);
+      final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      expect(cache.isUpToDate(), isTrue);
+    });
+    test('should update cached artifacts which are not up to date', () async {
+      final CachedArtifact artifact1 = MockCachedArtifact();
+      final CachedArtifact artifact2 = MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(false);
+      final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      await cache.updateAll(<DevelopmentArtifact>{});
+      verifyNever(artifact1.update(<DevelopmentArtifact>{}));
+      verify(artifact2.update(<DevelopmentArtifact>{}));
+    });
+    testUsingContext('failed storage.googleapis.com download shows China warning', () async {
+      final CachedArtifact artifact1 = MockCachedArtifact();
+      final CachedArtifact artifact2 = MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(false);
+      when(artifact2.isUpToDate()).thenReturn(false);
+      final MockInternetAddress address = MockInternetAddress();
+      when(address.host).thenReturn('storage.googleapis.com');
+      when(artifact1.update(<DevelopmentArtifact>{})).thenThrow(SocketException(
+        'Connection reset by peer',
+        address: address,
+      ));
+      final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      try {
+        await cache.updateAll(<DevelopmentArtifact>{});
+        fail('Mock thrown exception expected');
+      } catch (e) {
+        verify(artifact1.update(<DevelopmentArtifact>{}));
+        // Don't continue when retrieval fails.
+        verifyNever(artifact2.update(<DevelopmentArtifact>{}));
+        expect(
+          testLogger.errorText,
+          contains('https://flutter.dev/community/china'),
+        );
+      }
+    });
+  });
+
+  testUsingContext('flattenNameSubdirs', () {
+    expect(flattenNameSubdirs(Uri.parse('http://flutter.dev/foo/bar')), 'flutter.dev/foo/bar');
+    expect(flattenNameSubdirs(Uri.parse('http://docs.flutter.io/foo/bar')), 'docs.flutter.io/foo/bar');
+    expect(flattenNameSubdirs(Uri.parse('https://www.flutter.dev')), 'www.flutter.dev');
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MockFileSystem(),
+  });
+}
+
+class MockFileSystem extends ForwardingFileSystem {
+  MockFileSystem() : super(MemoryFileSystem());
+
+  @override
+  File file(dynamic path) {
+    return MockFile();
+  }
+}
+
+class MockFile extends Mock implements File {
+  @override
+  Future<RandomAccessFile> open({ FileMode mode = FileMode.read }) async {
+    return MockRandomAccessFile();
+  }
+}
+
+class MockRandomAccessFile extends Mock implements RandomAccessFile {}
+class MockCachedArtifact extends Mock implements CachedArtifact {}
+class MockInternetAddress extends Mock implements InternetAddress {}
+class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/channel_test.dart b/packages/flutter_tools/test/general.shard/channel_test.dart
new file mode 100644
index 0000000..64dd560
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/channel_test.dart
@@ -0,0 +1,244 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io' hide File;
+
+import 'package:args/command_runner.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/channel.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+Process createMockProcess({ int exitCode = 0, String stdout = '', String stderr = '' }) {
+  final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
+    utf8.encode(stdout),
+  ]);
+  final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
+    utf8.encode(stderr),
+  ]);
+  final Process process = MockProcess();
+
+  when(process.stdout).thenAnswer((_) => stdoutStream);
+  when(process.stderr).thenAnswer((_) => stderrStream);
+  when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
+  return process;
+}
+
+void main() {
+  group('channel', () {
+    final MockProcessManager mockProcessManager = MockProcessManager();
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    Future<void> simpleChannelTest(List<String> args) async {
+      final ChannelCommand command = ChannelCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(args);
+      expect(testLogger.errorText, hasLength(0));
+      // The bots may return an empty list of channels (network hiccup?)
+      // and when run locally the list of branches might be different
+      // so we check for the header text rather than any specific channel name.
+      expect(testLogger.statusText, contains('Flutter channels:'));
+    }
+
+    testUsingContext('list', () async {
+      await simpleChannelTest(<String>['channel']);
+    });
+
+    testUsingContext('verbose list', () async {
+      await simpleChannelTest(<String>['channel', '-v']);
+    });
+
+    testUsingContext('removes duplicates', () async {
+      final Process process = createMockProcess(
+          stdout: 'origin/dev\n'
+                  'origin/beta\n'
+                  'origin/stable\n'
+                  'upstream/dev\n'
+                  'upstream/beta\n'
+                  'upstream/stable\n');
+      when(mockProcessManager.start(
+        <String>['git', 'branch', '-r'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(process));
+
+      final ChannelCommand command = ChannelCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['channel']);
+
+      verify(mockProcessManager.start(
+        <String>['git', 'branch', '-r'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+
+      expect(testLogger.errorText, hasLength(0));
+
+      // format the status text for a simpler assertion.
+      final Iterable<String> rows = testLogger.statusText
+        .split('\n')
+        .map((String line) => line.trim())
+        .where((String line) => line?.isNotEmpty == true)
+        .skip(1); // remove `Flutter channels:` line
+
+      expect(rows, <String>['dev', 'beta', 'stable']);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('can switch channels', () async {
+      when(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/beta'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'checkout', 'beta', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+
+      final ChannelCommand command = ChannelCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['channel', 'beta']);
+
+      verify(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/beta'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'checkout', 'beta', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+
+      expect(testLogger.statusText, contains("Switching to flutter channel 'beta'..."));
+      expect(testLogger.errorText, hasLength(0));
+
+      when(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/stable'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'checkout', 'stable', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+
+      await runner.run(<String>['channel', 'stable']);
+
+      verify(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/stable'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'checkout', 'stable', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      FileSystem: () => MemoryFileSystem(),
+    });
+
+    // This verifies that bug https://github.com/flutter/flutter/issues/21134
+    // doesn't return.
+    testUsingContext('removes version stamp file when switching channels', () async {
+      when(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/beta'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+      when(mockProcessManager.start(
+        <String>['git', 'checkout', 'beta', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) => Future<Process>.value(createMockProcess()));
+
+      final File versionCheckFile = Cache.instance.getStampFileFor(
+        VersionCheckStamp.flutterVersionCheckStampFile,
+      );
+
+      /// Create a bogus "leftover" version check file to make sure it gets
+      /// removed when the channel changes. The content doesn't matter.
+      versionCheckFile.createSync(recursive: true);
+      versionCheckFile.writeAsStringSync('''
+        {
+          "lastTimeVersionWasChecked": "2151-08-29 10:17:30.763802",
+          "lastKnownRemoteVersion": "2151-09-26 15:56:19.000Z"
+        }
+      ''');
+
+      final ChannelCommand command = ChannelCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['channel', 'beta']);
+
+      verify(mockProcessManager.start(
+        <String>['git', 'fetch'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'show-ref', '--verify', '--quiet', 'refs/heads/beta'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+      verify(mockProcessManager.start(
+        <String>['git', 'checkout', 'beta', '--'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).called(1);
+
+      expect(testLogger.statusText, isNot(contains('A new version of Flutter')));
+      expect(testLogger.errorText, hasLength(0));
+      expect(versionCheckFile.existsSync(), isFalse);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      FileSystem: () => MemoryFileSystem(),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockProcess extends Mock implements Process {}
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart
new file mode 100644
index 0000000..05a49bf
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/analyze_continuously_test.dart
@@ -0,0 +1,105 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/dart/analysis.dart';
+import 'package:flutter_tools/src/dart/pub.dart';
+import 'package:flutter_tools/src/dart/sdk.dart';
+import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  AnalysisServer server;
+  Directory tempDir;
+
+  setUp(() {
+    FlutterCommandRunner.initFlutterRoot();
+    tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.');
+  });
+
+  tearDown(() {
+    tryToDelete(tempDir);
+    return server?.dispose();
+  });
+
+  group('analyze --watch', () {
+    testUsingContext('AnalysisServer success', () async {
+      _createSampleProject(tempDir);
+
+      await pubGet(context: PubContext.flutterTests, directory: tempDir.path);
+
+      server = AnalysisServer(dartSdkPath, <String>[tempDir.path]);
+
+      int errorCount = 0;
+      final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
+      server.onErrors.listen((FileAnalysisErrors errors) => errorCount += errors.errors.length);
+
+      await server.start();
+      await onDone;
+
+      expect(errorCount, 0);
+    }, overrides: <Type, Generator>{
+      OperatingSystemUtils: () => os,
+    });
+  });
+
+  testUsingContext('AnalysisServer errors', () async {
+    _createSampleProject(tempDir, brokenCode: true);
+
+    await pubGet(context: PubContext.flutterTests, directory: tempDir.path);
+
+    server = AnalysisServer(dartSdkPath, <String>[tempDir.path]);
+
+    int errorCount = 0;
+    final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
+    server.onErrors.listen((FileAnalysisErrors errors) {
+      errorCount += errors.errors.length;
+    });
+
+    await server.start();
+    await onDone;
+
+    expect(errorCount, greaterThan(0));
+  }, overrides: <Type, Generator>{
+    OperatingSystemUtils: () => os,
+  });
+
+  testUsingContext('Returns no errors when source is error-free', () async {
+    const String contents = "StringBuffer bar = StringBuffer('baz');";
+    tempDir.childFile('main.dart').writeAsStringSync(contents);
+    server = AnalysisServer(dartSdkPath, <String>[tempDir.path]);
+
+    int errorCount = 0;
+    final Future<bool> onDone = server.onAnalyzing.where((bool analyzing) => analyzing == false).first;
+    server.onErrors.listen((FileAnalysisErrors errors) {
+      errorCount += errors.errors.length;
+    });
+    await server.start();
+    await onDone;
+    expect(errorCount, 0);
+  }, overrides: <Type, Generator>{
+    OperatingSystemUtils: () => os,
+  });
+}
+
+void _createSampleProject(Directory directory, { bool brokenCode = false }) {
+  final File pubspecFile = fs.file(fs.path.join(directory.path, 'pubspec.yaml'));
+  pubspecFile.writeAsStringSync('''
+name: foo_project
+''');
+
+  final File dartFile = fs.file(fs.path.join(directory.path, 'lib', 'main.dart'));
+  dartFile.parent.createSync();
+  dartFile.writeAsStringSync('''
+void main() {
+  print('hello world');
+  ${brokenCode ? 'prints("hello world");' : ''}
+}
+''');
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart
new file mode 100644
index 0000000..d783d64
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/analyze_once_test.dart
@@ -0,0 +1,242 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/analyze.dart';
+import 'package:flutter_tools/src/commands/create.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+/// Test case timeout for tests involving project analysis.
+const Timeout allowForSlowAnalyzeTests = Timeout.factor(5.0);
+
+final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
+  Platform: _kNoColorTerminalPlatform,
+};
+
+void main() {
+  final String analyzerSeparator = platform.isWindows ? '-' : '•';
+
+  group('analyze once', () {
+    Directory tempDir;
+    String projectPath;
+    File libMain;
+
+    setUpAll(() {
+      Cache.disableLocking();
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_1.').absolute;
+      projectPath = fs.path.join(tempDir.path, 'flutter_project');
+      libMain = fs.file(fs.path.join(projectPath, 'lib', 'main.dart'));
+    });
+
+    tearDownAll(() {
+      tryToDelete(tempDir);
+    });
+
+    // Create a project to be analyzed
+    testUsingContext('flutter create', () async {
+      await runCommand(
+        command: CreateCommand(),
+        arguments: <String>['--no-wrap', 'create', projectPath],
+        statusTextContains: <String>[
+          'All done!',
+          'Your application code is in ${fs.path.normalize(fs.path.join(fs.path.relative(projectPath), 'lib', 'main.dart'))}',
+        ],
+      );
+      expect(libMain.existsSync(), isTrue);
+    }, timeout: allowForRemotePubInvocation);
+
+    // Analyze in the current directory - no arguments
+    testUsingContext('working directory', () async {
+      await runCommand(
+        command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
+        arguments: <String>['analyze'],
+        statusTextContains: <String>['No issues found!'],
+      );
+    }, timeout: allowForSlowAnalyzeTests);
+
+    // Analyze a specific file outside the current directory
+    testUsingContext('passing one file throws', () async {
+      await runCommand(
+        command: AnalyzeCommand(),
+        arguments: <String>['analyze', libMain.path],
+        toolExit: true,
+        exitMessageContains: 'is not a directory',
+      );
+    });
+
+    // Analyze in the current directory - no arguments
+    testUsingContext('working directory with errors', () async {
+      // Break the code to produce the "The parameter 'onPressed' is required" hint
+      // that is upgraded to a warning in package:flutter/analysis_options_user.yaml
+      // to assert that we are using the default Flutter analysis options.
+      // Also insert a statement that should not trigger a lint here
+      // but will trigger a lint later on when an analysis_options.yaml is added.
+      String source = await libMain.readAsString();
+      source = source.replaceFirst(
+        'onPressed: _incrementCounter,',
+        '// onPressed: _incrementCounter,',
+      );
+      source = source.replaceFirst(
+        '_counter++;',
+        '_counter++; throw "an error message";',
+      );
+      await libMain.writeAsString(source);
+
+      // Analyze in the current directory - no arguments
+      await runCommand(
+        command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
+        arguments: <String>['analyze'],
+        statusTextContains: <String>[
+          'Analyzing',
+          'warning $analyzerSeparator The parameter \'onPressed\' is required',
+          'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
+        ],
+        exitMessageContains: '2 issues found.',
+        toolExit: true,
+      );
+    }, timeout: allowForSlowAnalyzeTests, overrides: noColorTerminalOverride);
+
+    // Analyze in the current directory - no arguments
+    testUsingContext('working directory with local options', () async {
+      // Insert an analysis_options.yaml file in the project
+      // which will trigger a lint for broken code that was inserted earlier
+      final File optionsFile = fs.file(fs.path.join(projectPath, 'analysis_options.yaml'));
+      await optionsFile.writeAsString('''
+  include: package:flutter/analysis_options_user.yaml
+  linter:
+    rules:
+      - only_throw_errors
+  ''');
+
+      // Analyze in the current directory - no arguments
+      await runCommand(
+        command: AnalyzeCommand(workingDirectory: fs.directory(projectPath)),
+        arguments: <String>['analyze'],
+        statusTextContains: <String>[
+          'Analyzing',
+          'warning $analyzerSeparator The parameter \'onPressed\' is required',
+          'info $analyzerSeparator The method \'_incrementCounter\' isn\'t used',
+          'info $analyzerSeparator Only throw instances of classes extending either Exception or Error',
+        ],
+        exitMessageContains: '3 issues found.',
+        toolExit: true,
+      );
+    }, timeout: allowForSlowAnalyzeTests, overrides: noColorTerminalOverride);
+
+    testUsingContext('no duplicate issues', () async {
+      final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_2.').absolute;
+
+      try {
+        final File foo = fs.file(fs.path.join(tempDir.path, 'foo.dart'));
+        foo.writeAsStringSync('''
+import 'bar.dart';
+
+void foo() => bar();
+''');
+
+        final File bar = fs.file(fs.path.join(tempDir.path, 'bar.dart'));
+        bar.writeAsStringSync('''
+import 'dart:async'; // unused
+
+void bar() {
+}
+''');
+
+        // Analyze in the current directory - no arguments
+        await runCommand(
+          command: AnalyzeCommand(workingDirectory: tempDir),
+          arguments: <String>['analyze'],
+          statusTextContains: <String>[
+            'Analyzing',
+          ],
+          exitMessageContains: '1 issue found.',
+          toolExit: true,
+        );
+      } finally {
+        tryToDelete(tempDir);
+      }
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('returns no issues when source is error-free', () async {
+      const String contents = '''
+StringBuffer bar = StringBuffer('baz');
+''';
+      final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_3.');
+      tempDir.childFile('main.dart').writeAsStringSync(contents);
+      try {
+        await runCommand(
+          command: AnalyzeCommand(workingDirectory: fs.directory(tempDir)),
+          arguments: <String>['analyze'],
+          statusTextContains: <String>['No issues found!'],
+        );
+      } finally {
+        tryToDelete(tempDir);
+      }
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('returns no issues for todo comments', () async {
+      const String contents = '''
+// TODO(foobar):
+StringBuffer bar = StringBuffer('baz');
+''';
+      final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_analyze_once_test_4.');
+      tempDir.childFile('main.dart').writeAsStringSync(contents);
+      try {
+        await runCommand(
+          command: AnalyzeCommand(workingDirectory: fs.directory(tempDir)),
+          arguments: <String>['analyze'],
+          statusTextContains: <String>['No issues found!'],
+        );
+      } finally {
+        tryToDelete(tempDir);
+      }
+    }, overrides: noColorTerminalOverride);
+  });
+}
+
+void assertContains(String text, List<String> patterns) {
+  if (patterns == null) {
+    expect(text, isEmpty);
+  } else {
+    for (String pattern in patterns) {
+      expect(text, contains(pattern));
+    }
+  }
+}
+
+Future<void> runCommand({
+  FlutterCommand command,
+  List<String> arguments,
+  List<String> statusTextContains,
+  List<String> errorTextContains,
+  bool toolExit = false,
+  String exitMessageContains,
+}) async {
+  try {
+    arguments.insert(0, '--flutter-root=${Cache.flutterRoot}');
+    await createTestCommandRunner(command).run(arguments);
+    expect(toolExit, isFalse, reason: 'Expected ToolExit exception');
+  } on ToolExit catch (e) {
+    if (!toolExit) {
+      testLogger.clear();
+      rethrow;
+    }
+    if (exitMessageContains != null) {
+      expect(e.message, contains(exitMessageContains));
+    }
+  }
+  assertContains(testLogger.statusText, statusTextContains);
+  assertContains(testLogger.errorText, errorTextContains);
+
+  testLogger.clear();
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/analyze_test.dart b/packages/flutter_tools/test/general.shard/commands/analyze_test.dart
new file mode 100644
index 0000000..ee24615
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/analyze_test.dart
@@ -0,0 +1,53 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/analyze_base.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+const String _kFlutterRoot = '/data/flutter';
+
+void main() {
+  FileSystem fs;
+  Directory tempDir;
+
+  setUp(() {
+    fs = MemoryFileSystem();
+    fs.directory(_kFlutterRoot).createSync(recursive: true);
+    Cache.flutterRoot = _kFlutterRoot;
+    tempDir = fs.systemTempDirectory.createTempSync('flutter_analysis_test.');
+  });
+
+  tearDown(() {
+    tryToDelete(tempDir);
+  });
+
+  group('analyze', () {
+    testUsingContext('inRepo', () {
+      // Absolute paths
+      expect(inRepo(<String>[tempDir.path]), isFalse);
+      expect(inRepo(<String>[fs.path.join(tempDir.path, 'foo')]), isFalse);
+      expect(inRepo(<String>[Cache.flutterRoot]), isTrue);
+      expect(inRepo(<String>[fs.path.join(Cache.flutterRoot, 'foo')]), isTrue);
+
+      // Relative paths
+      fs.currentDirectory = Cache.flutterRoot;
+      expect(inRepo(<String>['.']), isTrue);
+      expect(inRepo(<String>['foo']), isTrue);
+      fs.currentDirectory = tempDir.path;
+      expect(inRepo(<String>['.']), isFalse);
+      expect(inRepo(<String>['foo']), isFalse);
+
+      // Ensure no exceptions
+      inRepo(null);
+      inRepo(<String>[]);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/assemble_test.dart b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
new file mode 100644
index 0000000..e18ebb5
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/assemble_test.dart
@@ -0,0 +1,84 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/build_system/build_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/assemble.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  group('Assemble', () {
+    Testbed testbed;
+    MockBuildSystem mockBuildSystem;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      mockBuildSystem = MockBuildSystem();
+      testbed = Testbed(overrides: <Type, Generator>{
+        BuildSystem: ()  => mockBuildSystem,
+      });
+    });
+
+    test('Can list the output directory relative to project root', () => testbed.run(() async {
+      final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
+      await commandRunner.run(<String>['assemble', '--flutter-root=.', 'build-dir', '-dBuildMode=debug']);
+      final BufferLogger bufferLogger = logger;
+      final Environment environment = Environment(
+        defines: <String, String>{
+          'BuildMode': 'debug'
+        }, projectDir: fs.currentDirectory,
+        buildDir: fs.directory(getBuildDirectory()),
+      );
+
+      expect(bufferLogger.statusText.trim(),
+          fs.path.relative(environment.buildDir.path, from: fs.currentDirectory.path));
+    }));
+
+    test('Can describe a target', () => testbed.run(() async {
+      when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[
+        <String, Object>{'fizz': 'bar'},
+      ]);
+      final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
+      await commandRunner.run(<String>['assemble', '--flutter-root=.', 'describe', 'foobar']);
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText.trim(), '[{"fizz":"bar"}]');
+    }));
+
+    test('Can describe a target\'s inputs', () => testbed.run(() async {
+      when(mockBuildSystem.describe('foobar', any)).thenReturn(<Map<String, Object>>[
+        <String, Object>{'name': 'foobar', 'inputs': <String>['bar', 'baz']},
+      ]);
+      final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
+      await commandRunner.run(<String>['assemble', '--flutter-root=.', 'inputs', 'foobar']);
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText.trim(), 'bar\nbaz');
+    }));
+
+    test('Can run a build', () => testbed.run(() async {
+      when(mockBuildSystem.build('foobar', any, any)).thenAnswer((Invocation invocation) async {
+        return BuildResult(true, const <String, ExceptionMeasurement>{}, const <String, PerformanceMeasurement>{});
+      });
+      final CommandRunner<void> commandRunner = createTestCommandRunner(AssembleCommand());
+      await commandRunner.run(<String>['assemble', 'run', 'foobar']);
+      final BufferLogger bufferLogger = logger;
+
+      expect(bufferLogger.statusText.trim(), 'build succeeded');
+    }));
+  });
+}
+
+class MockBuildSystem extends Mock implements BuildSystem {}
diff --git a/packages/flutter_tools/test/general.shard/commands/attach_test.dart b/packages/flutter_tools/test/general.shard/commands/attach_test.dart
new file mode 100644
index 0000000..c708f1e
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/attach_test.dart
@@ -0,0 +1,644 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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/common.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/base/terminal.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/attach.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+import 'package:multicast_dns/multicast_dns.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('attach', () {
+    StreamLogger logger;
+    FileSystem testFileSystem;
+
+    setUp(() {
+      Cache.disableLocking();
+      logger = StreamLogger();
+      testFileSystem = MemoryFileSystem(
+      style: platform.isWindows
+          ? FileSystemStyle.windows
+          : FileSystemStyle.posix,
+      );
+      testFileSystem.directory('lib').createSync();
+      testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync();
+    });
+
+    group('with one device and no specified target file', () {
+      const int devicePort = 499;
+      const int hostPort = 42;
+
+      MockDeviceLogReader mockLogReader;
+      MockPortForwarder portForwarder;
+      MockAndroidDevice device;
+
+      setUp(() {
+        mockLogReader = MockDeviceLogReader();
+        portForwarder = MockPortForwarder();
+        device = MockAndroidDevice();
+        when(device.getLogReader()).thenAnswer((_) {
+          // Now that the reader is used, start writing messages to it.
+          Timer.run(() {
+            mockLogReader.addLine('Foo');
+            mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
+          });
+
+          return mockLogReader;
+        });
+        when(device.portForwarder)
+          .thenReturn(portForwarder);
+        when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+          .thenAnswer((_) async => hostPort);
+        when(portForwarder.forwardedPorts)
+          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
+        when(portForwarder.unforward(any))
+          .thenAnswer((_) async => null);
+
+        // We cannot add the device to a device manager because that is
+        // only enabled by the context of each testUsingContext call.
+        //
+        // Instead each test will add the device to the device manager
+        // on its own.
+      });
+
+      tearDown(() {
+        mockLogReader.dispose();
+      });
+
+      testUsingContext('finds observatory port and forwards', () async {
+        testDeviceManager.addDevice(device);
+        final Completer<void> completer = Completer<void>();
+        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
+          if (message == '[stdout] Done.') {
+            // The "Done." message is output by the AttachCommand when it's done.
+            completer.complete();
+          }
+        });
+        final Future<void> task = createTestCommandRunner(AttachCommand()).run(<String>['attach']);
+        await completer.future;
+        verify(
+          portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')),
+        ).called(1);
+        await expectLoggerInterruptEndsTask(task, logger);
+        await loggerSubscription.cancel();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        Logger: () => logger,
+      });
+
+      testUsingContext('accepts filesystem parameters', () async {
+        testDeviceManager.addDevice(device);
+
+        const String filesystemScheme = 'foo';
+        const String filesystemRoot = '/build-output/';
+        const String projectRoot = '/build-output/project-root';
+        const String outputDill = '/tmp/output.dill';
+
+        final MockHotRunner mockHotRunner = MockHotRunner();
+        when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+            .thenAnswer((_) async => 0);
+
+        final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
+        when(
+          mockHotRunnerFactory.build(
+            any,
+            target: anyNamed('target'),
+            projectRootPath: anyNamed('projectRootPath'),
+            dillOutputPath: anyNamed('dillOutputPath'),
+            debuggingOptions: anyNamed('debuggingOptions'),
+            packagesFilePath: anyNamed('packagesFilePath'),
+            usesTerminalUi: anyNamed('usesTerminalUi'),
+            flutterProject: anyNamed('flutterProject'),
+            ipv6: false,
+          ),
+        ).thenReturn(mockHotRunner);
+
+        final AttachCommand command = AttachCommand(
+          hotRunnerFactory: mockHotRunnerFactory,
+        );
+        await createTestCommandRunner(command).run(<String>[
+          'attach',
+          '--filesystem-scheme',
+          filesystemScheme,
+          '--filesystem-root',
+          filesystemRoot,
+          '--project-root',
+          projectRoot,
+          '--output-dill',
+          outputDill,
+          '-v', // enables verbose logging
+        ]);
+
+        // Validate the attach call built a mock runner with the right
+        // project root and output dill.
+        final VerificationResult verificationResult = verify(
+          mockHotRunnerFactory.build(
+            captureAny,
+            target: anyNamed('target'),
+            projectRootPath: projectRoot,
+            dillOutputPath: outputDill,
+            debuggingOptions: anyNamed('debuggingOptions'),
+            packagesFilePath: anyNamed('packagesFilePath'),
+            usesTerminalUi: anyNamed('usesTerminalUi'),
+            flutterProject: anyNamed('flutterProject'),
+            ipv6: false,
+          ),
+        )..called(1);
+
+        final List<FlutterDevice> flutterDevices = verificationResult.captured.first;
+        expect(flutterDevices, hasLength(1));
+
+        // Validate that the attach call built a flutter device with the right
+        // output dill, filesystem scheme, and filesystem root.
+        final FlutterDevice flutterDevice = flutterDevices.first;
+
+        expect(flutterDevice.dillOutputPath, outputDill);
+        expect(flutterDevice.fileSystemScheme, filesystemScheme);
+        expect(flutterDevice.fileSystemRoots, const <String>[filesystemRoot]);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+      });
+
+      testUsingContext('exits when ipv6 is specified and debug-port is not', () async {
+        testDeviceManager.addDevice(device);
+
+        final AttachCommand command = AttachCommand();
+        await expectLater(
+          createTestCommandRunner(command).run(<String>['attach', '--ipv6']),
+          throwsToolExit(
+            message: 'When the --debug-port or --debug-uri is unknown, this command determines '
+                     'the value of --ipv6 on its own.',
+          ),
+        );
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+      },);
+
+      testUsingContext('exits when observatory-port is specified and debug-port is not', () async {
+        testDeviceManager.addDevice(device);
+
+        final AttachCommand command = AttachCommand();
+        await expectLater(
+          createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']),
+          throwsToolExit(
+            message: 'When the --debug-port or --debug-uri is unknown, this command does not use '
+                     'the value of --observatory-port.',
+          ),
+        );
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+      },);
+    });
+
+
+    testUsingContext('selects specified target', () async {
+      const int devicePort = 499;
+      const int hostPort = 42;
+      final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
+      final MockPortForwarder portForwarder = MockPortForwarder();
+      final MockAndroidDevice device = MockAndroidDevice();
+      final MockHotRunner mockHotRunner = MockHotRunner();
+      final MockHotRunnerFactory mockHotRunnerFactory = MockHotRunnerFactory();
+      when(device.portForwarder)
+        .thenReturn(portForwarder);
+      when(portForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
+        .thenAnswer((_) async => hostPort);
+      when(portForwarder.forwardedPorts)
+        .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
+      when(portForwarder.unforward(any))
+        .thenAnswer((_) async => null);
+      when(mockHotRunner.attach(appStartedCompleter: anyNamed('appStartedCompleter')))
+          .thenAnswer((_) async => 0);
+      when(mockHotRunnerFactory.build(
+        any,
+        target: anyNamed('target'),
+        debuggingOptions: anyNamed('debuggingOptions'),
+        packagesFilePath: anyNamed('packagesFilePath'),
+        usesTerminalUi: anyNamed('usesTerminalUi'),
+        flutterProject: anyNamed('flutterProject'),
+        ipv6: false,
+      )).thenReturn(mockHotRunner);
+
+      testDeviceManager.addDevice(device);
+      when(device.getLogReader())
+        .thenAnswer((_) {
+          // Now that the reader is used, start writing messages to it.
+          Timer.run(() {
+            mockLogReader.addLine('Foo');
+            mockLogReader.addLine(
+                'Observatory listening on http://127.0.0.1:$devicePort');
+          });
+          return mockLogReader;
+        });
+      final File foo = fs.file('lib/foo.dart')
+        ..createSync();
+
+      // Delete the main.dart file to be sure that attach works without it.
+      fs.file(fs.path.join('lib', 'main.dart')).deleteSync();
+
+      final AttachCommand command = AttachCommand(hotRunnerFactory: mockHotRunnerFactory);
+      await createTestCommandRunner(command).run(<String>['attach', '-t', foo.path, '-v']);
+
+      verify(mockHotRunnerFactory.build(
+        any,
+        target: foo.path,
+        debuggingOptions: anyNamed('debuggingOptions'),
+        packagesFilePath: anyNamed('packagesFilePath'),
+        usesTerminalUi: anyNamed('usesTerminalUi'),
+        flutterProject: anyNamed('flutterProject'),
+        ipv6: false,
+      )).called(1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    group('forwarding to given port', () {
+      const int devicePort = 499;
+      const int hostPort = 42;
+      MockPortForwarder portForwarder;
+      MockAndroidDevice device;
+
+      setUp(() {
+        portForwarder = MockPortForwarder();
+        device = MockAndroidDevice();
+
+        when(device.portForwarder)
+          .thenReturn(portForwarder);
+        when(portForwarder.forward(devicePort))
+          .thenAnswer((_) async => hostPort);
+        when(portForwarder.forwardedPorts)
+          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
+        when(portForwarder.unforward(any))
+          .thenAnswer((_) async => null);
+      });
+
+      testUsingContext('succeeds in ipv4 mode', () async {
+        testDeviceManager.addDevice(device);
+
+        final Completer<void> completer = Completer<void>();
+        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
+          if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
+            // Wait until resident_runner.dart tries to connect.
+            // There's nothing to connect _to_, so that's as far as we care to go.
+            completer.complete();
+          }
+        });
+        final Future<void> task = createTestCommandRunner(AttachCommand())
+          .run(<String>['attach', '--debug-port', '$devicePort']);
+        await completer.future;
+        verify(portForwarder.forward(devicePort)).called(1);
+
+        await expectLoggerInterruptEndsTask(task, logger);
+        await loggerSubscription.cancel();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        Logger: () => logger,
+      });
+
+      testUsingContext('succeeds in ipv6 mode', () async {
+        testDeviceManager.addDevice(device);
+
+        final Completer<void> completer = Completer<void>();
+        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
+          if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
+            // Wait until resident_runner.dart tries to connect.
+            // There's nothing to connect _to_, so that's as far as we care to go.
+            completer.complete();
+          }
+        });
+        final Future<void> task = createTestCommandRunner(AttachCommand())
+          .run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']);
+        await completer.future;
+        verify(portForwarder.forward(devicePort)).called(1);
+
+        await expectLoggerInterruptEndsTask(task, logger);
+        await loggerSubscription.cancel();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        Logger: () => logger,
+      });
+
+      testUsingContext('skips in ipv4 mode with a provided observatory port', () async {
+        testDeviceManager.addDevice(device);
+
+        final Completer<void> completer = Completer<void>();
+        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
+          if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') {
+            // Wait until resident_runner.dart tries to connect.
+            // There's nothing to connect _to_, so that's as far as we care to go.
+            completer.complete();
+          }
+        });
+        final Future<void> task = createTestCommandRunner(AttachCommand()).run(
+          <String>[
+            'attach',
+            '--debug-port',
+            '$devicePort',
+            '--observatory-port',
+            '$hostPort',
+          ],
+        );
+        await completer.future;
+        verifyNever(portForwarder.forward(devicePort));
+
+        await expectLoggerInterruptEndsTask(task, logger);
+        await loggerSubscription.cancel();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        Logger: () => logger,
+      });
+
+      testUsingContext('skips in ipv6 mode with a provided observatory port', () async {
+        testDeviceManager.addDevice(device);
+
+        final Completer<void> completer = Completer<void>();
+        final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) {
+          if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') {
+            // Wait until resident_runner.dart tries to connect.
+            // There's nothing to connect _to_, so that's as far as we care to go.
+            completer.complete();
+          }
+        });
+        final Future<void> task = createTestCommandRunner(AttachCommand()).run(
+          <String>[
+            'attach',
+            '--debug-port',
+            '$devicePort',
+            '--observatory-port',
+            '$hostPort',
+            '--ipv6',
+          ],
+        );
+        await completer.future;
+        verifyNever(portForwarder.forward(devicePort));
+
+        await expectLoggerInterruptEndsTask(task, logger);
+        await loggerSubscription.cancel();
+      }, overrides: <Type, Generator>{
+        FileSystem: () => testFileSystem,
+        Logger: () => logger,
+      });
+    });
+
+    testUsingContext('exits when no device connected', () async {
+      final AttachCommand command = AttachCommand();
+      await expectLater(
+        createTestCommandRunner(command).run(<String>['attach']),
+        throwsA(isInstanceOf<ToolExit>()),
+      );
+      expect(testLogger.statusText, contains('No supported devices connected'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+
+    testUsingContext('exits when multiple devices connected', () async {
+      Device aDeviceWithId(String id) {
+        final MockAndroidDevice device = MockAndroidDevice();
+        when(device.name).thenReturn('d$id');
+        when(device.id).thenReturn(id);
+        when(device.isLocalEmulator).thenAnswer((_) async => false);
+        when(device.sdkNameAndVersion).thenAnswer((_) async => 'Android 46');
+        return device;
+      }
+
+      final AttachCommand command = AttachCommand();
+      testDeviceManager.addDevice(aDeviceWithId('xx1'));
+      testDeviceManager.addDevice(aDeviceWithId('yy2'));
+      await expectLater(
+        createTestCommandRunner(command).run(<String>['attach']),
+        throwsA(isInstanceOf<ToolExit>()),
+      );
+      expect(testLogger.statusText, contains('More than one device'));
+      expect(testLogger.statusText, contains('xx1'));
+      expect(testLogger.statusText, contains('yy2'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+    });
+  });
+
+  group('mDNS Discovery', () {
+    final int year3000 = DateTime(3000).millisecondsSinceEpoch;
+
+    MDnsClient getMockClient(
+      List<PtrResourceRecord> ptrRecords,
+      Map<String, List<SrvResourceRecord>> srvResponse,
+    ) {
+      final MDnsClient client = MockMDnsClient();
+
+      when(client.lookup<PtrResourceRecord>(
+        ResourceRecordQuery.serverPointer(MDnsObservatoryDiscovery.dartObservatoryName),
+      )).thenAnswer((_) => Stream<PtrResourceRecord>.fromIterable(ptrRecords));
+
+      for (final MapEntry<String, List<SrvResourceRecord>> entry in srvResponse.entries) {
+        when(client.lookup<SrvResourceRecord>(
+          ResourceRecordQuery.service(entry.key),
+        )).thenAnswer((_) => Stream<SrvResourceRecord>.fromIterable(entry.value));
+      }
+      return client;
+    }
+
+    testUsingContext('No ports available', () async {
+      final MDnsClient client = getMockClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      final int port = (await portDiscovery.query())?.port;
+      expect(port, isNull);
+    });
+
+    testUsingContext('One port available, no appId', () async {
+      final MDnsClient client = getMockClient(
+        <PtrResourceRecord>[
+          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+        ],
+        <String, List<SrvResourceRecord>>{
+          'bar': <SrvResourceRecord>[
+            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          ],
+        },
+      );
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      final int port = (await portDiscovery.query())?.port;
+      expect(port, 123);
+    });
+
+    testUsingContext('Multiple ports available, without appId', () async {
+      final MDnsClient client = getMockClient(
+        <PtrResourceRecord>[
+          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
+        ],
+        <String, List<SrvResourceRecord>>{
+          'bar': <SrvResourceRecord>[
+            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          ],
+          'fiz': <SrvResourceRecord>[
+            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
+          ],
+        },
+      );
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      expect(() => portDiscovery.query(), throwsToolExit());
+    });
+
+    testUsingContext('Multiple ports available, with appId', () async {
+      final MDnsClient client = getMockClient(
+        <PtrResourceRecord>[
+          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
+        ],
+        <String, List<SrvResourceRecord>>{
+          'bar': <SrvResourceRecord>[
+            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          ],
+          'fiz': <SrvResourceRecord>[
+            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
+          ],
+        },
+      );
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port;
+      expect(port, 321);
+    });
+
+    testUsingContext('Multiple ports available per process, with appId', () async {
+      final MDnsClient client = getMockClient(
+        <PtrResourceRecord>[
+          PtrResourceRecord('foo', year3000, domainName: 'bar'),
+          PtrResourceRecord('baz', year3000, domainName: 'fiz'),
+        ],
+        <String, List<SrvResourceRecord>>{
+          'bar': <SrvResourceRecord>[
+            SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'),
+            SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'),
+          ],
+          'fiz': <SrvResourceRecord>[
+            SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'),
+            SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'),
+          ],
+        },
+      );
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
+      expect(port, 1234);
+    });
+
+    testUsingContext('Query returns null', () async {
+      final MDnsClient client = getMockClient(
+        <PtrResourceRecord>[],
+         <String, List<SrvResourceRecord>>{},
+      );
+
+      final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery(mdnsClient: client);
+      final int port = (await portDiscovery.query(applicationId: 'bar'))?.port;
+      expect(port, isNull);
+    });
+  });
+}
+
+class MockMDnsClient extends Mock implements MDnsClient {}
+
+class MockPortForwarder extends Mock implements DevicePortForwarder {}
+
+class MockHotRunner extends Mock implements HotRunner {}
+
+class MockHotRunnerFactory extends Mock implements HotRunnerFactory {}
+
+class StreamLogger extends Logger {
+  @override
+  bool get isVerbose => true;
+
+  @override
+  void printError(
+    String message, {
+    StackTrace stackTrace,
+    bool emphasis,
+    TerminalColor color,
+    int indent,
+    int hangingIndent,
+    bool wrap,
+  }) {
+    _log('[stderr] $message');
+  }
+
+  @override
+  void printStatus(
+    String message, {
+    bool emphasis,
+    TerminalColor color,
+    bool newline,
+    int indent,
+    int hangingIndent,
+    bool wrap,
+  }) {
+    _log('[stdout] $message');
+  }
+
+  @override
+  void printTrace(String message) {
+    _log('[verbose] $message');
+  }
+
+  @override
+  Status startProgress(
+    String message, {
+    @required Duration timeout,
+    String progressId,
+    bool multilineOutput = false,
+    int progressIndicatorPadding = kDefaultStatusPadding,
+  }) {
+    _log('[progress] $message');
+    return SilentStatus(timeout: timeout)..start();
+  }
+
+  bool _interrupt = false;
+
+  void interrupt() {
+    _interrupt = true;
+  }
+
+  final StreamController<String> _controller = StreamController<String>.broadcast();
+
+  void _log(String message) {
+    _controller.add(message);
+    if (_interrupt) {
+      _interrupt = false;
+      throw const LoggerInterrupted();
+    }
+  }
+
+  Stream<String> get stream => _controller.stream;
+}
+
+class LoggerInterrupted implements Exception {
+  const LoggerInterrupted();
+}
+
+Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async {
+  logger.interrupt(); // an exception during the task should cause it to fail...
+  try {
+    await task;
+    expect(false, isTrue); // (shouldn't reach here)
+  } on ToolExit catch (error) {
+    expect(error.exitCode, 2); // ...with exit code 2.
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
new file mode 100644
index 0000000..6c6c8ad
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_bundle_test.dart
@@ -0,0 +1,96 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build_bundle.dart';
+import 'package:flutter_tools/src/bundle.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  Cache.disableLocking();
+
+  group('getUsage', () {
+    Directory tempDir;
+    MockBundleBuilder mockBundleBuilder;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
+
+      mockBundleBuilder = MockBundleBuilder();
+      when(
+        mockBundleBuilder.build(
+          platform: anyNamed('platform'),
+          buildMode: anyNamed('buildMode'),
+          mainPath: anyNamed('mainPath'),
+          manifestPath: anyNamed('manifestPath'),
+          applicationKernelFilePath: anyNamed('applicationKernelFilePath'),
+          depfilePath: anyNamed('depfilePath'),
+          privateKeyPath: anyNamed('privateKeyPath'),
+          assetDirPath: anyNamed('assetDirPath'),
+          packagesPath: anyNamed('packagesPath'),
+          precompiledSnapshot: anyNamed('precompiledSnapshot'),
+          reportLicensedPackages: anyNamed('reportLicensedPackages'),
+          trackWidgetCreation: anyNamed('trackWidgetCreation'),
+          extraFrontEndOptions: anyNamed('extraFrontEndOptions'),
+          extraGenSnapshotOptions: anyNamed('extraGenSnapshotOptions'),
+          fileSystemRoots: anyNamed('fileSystemRoots'),
+          fileSystemScheme: anyNamed('fileSystemScheme'),
+        ),
+      ).thenAnswer((_) => Future<void>.value());
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    Future<BuildBundleCommand> runCommandIn(String projectPath, { List<String> arguments }) async {
+      final BuildBundleCommand command = BuildBundleCommand(bundleBuilder: mockBundleBuilder);
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>[
+        'bundle',
+        ...?arguments,
+        '--target=$projectPath/lib/main.dart',
+      ]);
+      return command;
+    }
+
+    testUsingContext('indicate that project is a module', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=module']);
+
+      final BuildBundleCommand command = await runCommandIn(projectPath);
+
+      expect(await command.usageValues,
+          containsPair(kCommandBuildBundleIsModule, 'true'));
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate that project is not a module', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=app']);
+
+      final BuildBundleCommand command = await runCommandIn(projectPath);
+
+      expect(await command.usageValues,
+          containsPair(kCommandBuildBundleIsModule, 'false'));
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate the target platform', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=app']);
+
+      final BuildBundleCommand command = await runCommandIn(projectPath);
+
+      expect(await command.usageValues,
+          containsPair(kCommandBuildBundleTargetPlatform, 'android-arm'));
+    }, timeout: allowForCreateFlutterProject);
+  });
+}
+
+class MockBundleBuilder extends Mock implements BundleBuilder {}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart b/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart
new file mode 100644
index 0000000..3734d04
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_fuchsia_test.dart
@@ -0,0 +1,237 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  Cache.disableLocking();
+
+  MemoryFileSystem memoryFileSystem;
+  MockPlatform linuxPlatform;
+  MockPlatform windowsPlatform;
+  MockFuchsiaSdk fuchsiaSdk;
+  MockFuchsiaArtifacts fuchsiaArtifacts;
+  MockFuchsiaArtifacts fuchsiaArtifactsNoCompiler;
+
+  setUp(() {
+    memoryFileSystem = MemoryFileSystem();
+    linuxPlatform = MockPlatform();
+    windowsPlatform = MockPlatform();
+    fuchsiaSdk = MockFuchsiaSdk();
+    fuchsiaArtifacts = MockFuchsiaArtifacts();
+    fuchsiaArtifactsNoCompiler = MockFuchsiaArtifacts();
+
+    when(linuxPlatform.isLinux).thenReturn(true);
+    when(windowsPlatform.isWindows).thenReturn(true);
+    when(windowsPlatform.isLinux).thenReturn(false);
+    when(windowsPlatform.isMacOS).thenReturn(false);
+    when(fuchsiaArtifacts.kernelCompiler).thenReturn(MockFile());
+    when(fuchsiaArtifactsNoCompiler.kernelCompiler).thenReturn(null);
+  });
+
+  group('Fuchsia build fails gracefully when', () {
+    testUsingContext('there is no Fuchsia project',
+        () async {
+      final BuildCommand command = BuildCommand();
+      applyMocksToCommand(command);
+      expect(
+          createTestCommandRunner(command)
+              .run(const <String>['build', 'fuchsia']),
+          throwsA(isInstanceOf<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      Platform: () => linuxPlatform,
+      FileSystem: () => memoryFileSystem,
+      FuchsiaArtifacts: () => fuchsiaArtifacts,
+    });
+
+    testUsingContext('there is no cmx file', () async {
+      final BuildCommand command = BuildCommand();
+      applyMocksToCommand(command);
+      fs.directory('fuchsia').createSync(recursive: true);
+      fs.file('.packages').createSync();
+      fs.file('pubspec.yaml').createSync();
+
+      expect(
+          createTestCommandRunner(command)
+              .run(const <String>['build', 'fuchsia']),
+          throwsA(isInstanceOf<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      Platform: () => linuxPlatform,
+      FileSystem: () => memoryFileSystem,
+      FuchsiaArtifacts: () => fuchsiaArtifacts,
+    });
+
+    testUsingContext('on Windows platform', () async {
+      final BuildCommand command = BuildCommand();
+      applyMocksToCommand(command);
+      const String appName = 'app_name';
+      fs
+          .file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync('{}');
+      fs.file('.packages').createSync();
+      final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+      pubspecFile.writeAsStringSync('name: $appName');
+
+      expect(
+          createTestCommandRunner(command)
+              .run(const <String>['build', 'fuchsia']),
+          throwsA(isInstanceOf<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      Platform: () => windowsPlatform,
+      FileSystem: () => memoryFileSystem,
+      FuchsiaArtifacts: () => fuchsiaArtifacts,
+    });
+
+    testUsingContext('there is no Fuchsia kernel compiler', () async {
+      final BuildCommand command = BuildCommand();
+      applyMocksToCommand(command);
+      const String appName = 'app_name';
+      fs
+          .file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync('{}');
+      fs.file('.packages').createSync();
+      fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+      final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+      pubspecFile.writeAsStringSync('name: $appName');
+      expect(
+          createTestCommandRunner(command)
+              .run(const <String>['build', 'fuchsia']),
+          throwsA(isInstanceOf<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      Platform: () => linuxPlatform,
+      FileSystem: () => memoryFileSystem,
+      FuchsiaArtifacts: () => fuchsiaArtifactsNoCompiler,
+    });
+  });
+
+  testUsingContext('Fuchsia build parts fit together right', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    const String appName = 'app_name';
+    fs
+        .file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('{}');
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+    final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+    pubspecFile.writeAsStringSync('name: $appName');
+
+    await createTestCommandRunner(command)
+        .run(const <String>['build', 'fuchsia']);
+    final String farPath =
+        fs.path.join(getFuchsiaBuildDirectory(), 'pkg', 'app_name-0.far');
+    expect(fs.file(farPath).existsSync(), isTrue);
+  }, overrides: <Type, Generator>{
+    Platform: () => linuxPlatform,
+    FileSystem: () => memoryFileSystem,
+    FuchsiaSdk: () => fuchsiaSdk,
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': '/',
+  };
+}
+
+class MockFuchsiaPM extends Mock implements FuchsiaPM {
+  String _appName;
+
+  @override
+  Future<bool> init(String buildPath, String appName) async {
+    if (!fs.directory(buildPath).existsSync()) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, 'meta', 'package'))
+        .createSync(recursive: true);
+    _appName = appName;
+    return true;
+  }
+
+  @override
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync()) {
+      return false;
+    }
+    fs.file(outKeyPath).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    fs.file(fs.path.join(buildPath, 'meta.far')).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    if (_appName == null) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, '$_appName-0.far'))
+        .createSync(recursive: true);
+    return true;
+  }
+}
+
+class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler {
+  @override
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    final String outDir = getFuchsiaBuildDirectory();
+    final String appName = fuchsiaProject.project.manifest.appName;
+    final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest');
+    fs.file(manifestPath).createSync(recursive: true);
+  }
+}
+
+class MockFuchsiaSdk extends Mock implements FuchsiaSdk {
+  @override
+  final FuchsiaPM fuchsiaPM = MockFuchsiaPM();
+
+  @override
+  final FuchsiaKernelCompiler fuchsiaKernelCompiler =
+      MockFuchsiaKernelCompiler();
+}
+
+class MockFile extends Mock implements File {}
+
+class MockFuchsiaArtifacts extends Mock implements FuchsiaArtifacts {}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart b/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart
new file mode 100644
index 0000000..638a20b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_linux_test.dart
@@ -0,0 +1,125 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/linux/makefile.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  MockProcessManager mockProcessManager;
+  MockProcess mockProcess;
+  MockPlatform linuxPlatform;
+  MockPlatform notLinuxPlatform;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+    mockProcess = MockProcess();
+    linuxPlatform = MockPlatform();
+    notLinuxPlatform = MockPlatform();
+    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async {
+      return 0;
+    });
+    when(mockProcess.stderr).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(mockProcess.stdout).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(linuxPlatform.isLinux).thenReturn(true);
+    when(notLinuxPlatform.isLinux).thenReturn(false);
+  });
+
+  testUsingContext('Linux build fails when there is no linux project', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'linux']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => linuxPlatform,
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('Linux build fails on non-linux platform', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file('linux/build.sh').createSync(recursive: true);
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'linux']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => notLinuxPlatform,
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('Linux build invokes make and writes temporary files', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file('linux/build.sh').createSync(recursive: true);
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+
+    when(mockProcessManager.start(<String>[
+      'make',
+      '-C',
+      '/linux',
+    ], runInShell: true)).thenAnswer((Invocation invocation) async {
+      return mockProcess;
+    });
+
+    await createTestCommandRunner(command).run(
+      const <String>['build', 'linux']
+    );
+    expect(fs.file('linux/flutter/generated_config').existsSync(), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+    ProcessManager: () => mockProcessManager,
+    Platform: () => linuxPlatform,
+  });
+
+  testUsingContext('linux can extract binary name from Makefile', () async {
+    fs.file('linux/Makefile')
+      ..createSync(recursive: true)
+      ..writeAsStringSync(r'''
+# Comment
+SOMETHING_ELSE=FOO
+BINARY_NAME=fizz_bar
+''');
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(makefileExecutableName(flutterProject.linux), 'fizz_bar');
+  }, overrides: <Type, Generator>{FileSystem: () => MemoryFileSystem()});
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': '/',
+  };
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
new file mode 100644
index 0000000..ceb07b8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_macos_test.dart
@@ -0,0 +1,117 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  MockProcessManager mockProcessManager;
+  MemoryFileSystem memoryFilesystem;
+  MockProcess mockProcess;
+  MockPlatform macosPlatform;
+  MockPlatform notMacosPlatform;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+    memoryFilesystem = MemoryFileSystem();
+    mockProcess = MockProcess();
+    macosPlatform = MockPlatform();
+    notMacosPlatform = MockPlatform();
+    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async {
+    return 0;
+    });
+    when(mockProcess.stderr).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(mockProcess.stdout).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(macosPlatform.isMacOS).thenReturn(true);
+    when(notMacosPlatform.isMacOS).thenReturn(false);
+  });
+
+  testUsingContext('macOS build fails when there is no macos project', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'macos']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => macosPlatform,
+  });
+
+  testUsingContext('macOS build fails on non-macOS platform', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'macos']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => notMacosPlatform,
+    FileSystem: () => memoryFilesystem,
+  });
+
+  testUsingContext('macOS build invokes build script', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.directory('macos').createSync();
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.currentDirectory);
+    final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory());
+
+    when(mockProcessManager.start(<String>[
+      '/usr/bin/env',
+      'xcrun',
+      'xcodebuild',
+      '-workspace', flutterProject.macos.xcodeWorkspace.path,
+      '-configuration', 'Debug',
+      '-scheme', 'Runner',
+      '-derivedDataPath', flutterBuildDir.absolute.path,
+      'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
+      'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
+    ], runInShell: true)).thenAnswer((Invocation invocation) async {
+      return mockProcess;
+    });
+
+    await createTestCommandRunner(command).run(
+      const <String>['build', 'macos']
+    );
+  }, overrides: <Type, Generator>{
+    FileSystem: () => memoryFilesystem,
+    ProcessManager: () => mockProcessManager,
+    Platform: () => macosPlatform,
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': '/',
+  };
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_web_test.dart b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart
new file mode 100644
index 0000000..2adb818
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_web_test.dart
@@ -0,0 +1,97 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/resident_web_runner.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:flutter_tools/src/web/compile.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  MockWebCompilationProxy mockWebCompilationProxy;
+  Testbed testbed;
+  MockPlatform mockPlatform;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  setUp(() {
+    mockWebCompilationProxy = MockWebCompilationProxy();
+    testbed = Testbed(setup: () {
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync('name: foo\n');
+      fs.file('.packages').createSync();
+      fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true);
+      fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+      when(mockWebCompilationProxy.initialize(
+        projectDirectory: anyNamed('projectDirectory'),
+        release: anyNamed('release')
+      )).thenAnswer((Invocation invocation) {
+        final String path = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib', 'main_web_entrypoint.dart.js');
+        fs.file(path).createSync(recursive: true);
+        fs.file('$path.map').createSync();
+        return Future<bool>.value(true);
+      });
+    }, overrides: <Type, Generator>{
+      WebCompilationProxy: () => mockWebCompilationProxy,
+      Platform: () => mockPlatform,
+      FlutterVersion: () => MockFlutterVersion(),
+    });
+  });
+
+  test('Refuses to build for web when missing index.html', () => testbed.run(() async {
+    fs.file(fs.path.join('web', 'index.html')).deleteSync();
+
+    expect(buildWeb(
+      FlutterProject.current(),
+      fs.path.join('lib', 'main.dart'),
+      BuildInfo.debug,
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }));
+
+  test('Refuses to build using runner when missing index.html', () => testbed.run(() async {
+    fs.file(fs.path.join('web', 'index.html')).deleteSync();
+
+    final ResidentWebRunner runner = ResidentWebRunner(
+      <FlutterDevice>[],
+      flutterProject: FlutterProject.current(),
+      ipv6: false,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+    );
+    expect(await runner.run(), 1);
+  }));
+
+  test('Can build for web', () => testbed.run(() async {
+
+    await buildWeb(
+      FlutterProject.current(),
+      fs.path.join('lib', 'main.dart'),
+      BuildInfo.debug,
+    );
+  }));
+}
+
+class MockWebCompilationProxy extends Mock implements WebCompilationProxy {}
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': '/',
+  };
+}
+class MockFlutterVersion extends Mock implements FlutterVersion {
+  @override
+  bool get isMaster => true;
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart b/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart
new file mode 100644
index 0000000..301fa7d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_windows_test.dart
@@ -0,0 +1,146 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:xml/xml.dart' as xml;
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  MockProcessManager mockProcessManager;
+  MemoryFileSystem memoryFilesystem;
+  MockProcess mockProcess;
+  MockPlatform windowsPlatform;
+  MockPlatform notWindowsPlatform;
+  MockVisualStudio mockVisualStudio;
+  const String solutionPath = r'C:\windows\Runner.sln';
+  const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community';
+  const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat';
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+    memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows);
+    mockProcess = MockProcess();
+    windowsPlatform = MockPlatform()
+        ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
+    notWindowsPlatform = MockPlatform();
+    mockVisualStudio = MockVisualStudio();
+    when(mockProcess.exitCode).thenAnswer((Invocation invocation) async {
+      return 0;
+    });
+    when(mockProcess.stderr).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(mockProcess.stdout).thenAnswer((Invocation invocation) {
+      return const Stream<List<int>>.empty();
+    });
+    when(windowsPlatform.isWindows).thenReturn(true);
+    when(notWindowsPlatform.isWindows).thenReturn(false);
+  });
+
+  testUsingContext('Windows build fails when there is no vcvars64.bat', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file(solutionPath).createSync(recursive: true);
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'windows']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => windowsPlatform,
+    FileSystem: () => memoryFilesystem,
+    VisualStudio: () => mockVisualStudio,
+  });
+
+  testUsingContext('Windows build fails when there is no windows project', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'windows']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => windowsPlatform,
+    FileSystem: () => memoryFilesystem,
+    VisualStudio: () => mockVisualStudio,
+  });
+
+  testUsingContext('Windows build fails on non windows platform', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file(solutionPath).createSync(recursive: true);
+    when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+
+    expect(createTestCommandRunner(command).run(
+      const <String>['build', 'windows']
+    ), throwsA(isInstanceOf<ToolExit>()));
+  }, overrides: <Type, Generator>{
+    Platform: () => notWindowsPlatform,
+    FileSystem: () => memoryFilesystem,
+    VisualStudio: () => mockVisualStudio,
+  });
+
+  testUsingContext('Windows build invokes msbuild and writes generated files', () async {
+    final BuildCommand command = BuildCommand();
+    applyMocksToCommand(command);
+    fs.file(solutionPath).createSync(recursive: true);
+    when(mockVisualStudio.vcvarsPath).thenReturn(vcvarsPath);
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+
+    when(mockProcessManager.start(<String>[
+      r'C:\packages\flutter_tools\bin\vs_build.bat',
+      vcvarsPath,
+      fs.path.basename(solutionPath),
+      'Release',
+    ], workingDirectory: fs.path.dirname(solutionPath))).thenAnswer((Invocation invocation) async {
+      return mockProcess;
+    });
+
+    await createTestCommandRunner(command).run(
+      const <String>['build', 'windows']
+    );
+
+    // Spot-check important elements from the properties file.
+    final File propsFile = fs.file(r'C:\windows\flutter\Generated.props');
+    expect(propsFile.existsSync(), true);
+    final xml.XmlDocument props = xml.parse(propsFile.readAsStringSync());
+    expect(props.findAllElements('PropertyGroup').first.getAttribute('Label'), 'UserMacros');
+    expect(props.findAllElements('ItemGroup').length, 1);
+    expect(props.findAllElements('FLUTTER_ROOT').first.text, r'C:\');
+  }, overrides: <Type, Generator>{
+    FileSystem: () => memoryFilesystem,
+    ProcessManager: () => mockProcessManager,
+    Platform: () => windowsPlatform,
+    VisualStudio: () => mockVisualStudio,
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': r'C:\',
+  };
+}
+class MockVisualStudio extends Mock implements VisualStudio {}
diff --git a/packages/flutter_tools/test/general.shard/commands/clean_test.dart b/packages/flutter_tools/test/general.shard/commands/clean_test.dart
new file mode 100644
index 0000000..85926c7
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/clean_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/commands/clean.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+
+void main() {
+  final MockFileSystem mockFileSystem = MockFileSystem();
+  final MockDirectory currentDirectory = MockDirectory();
+  final MockDirectory exampleDirectory = MockDirectory();
+  final MockDirectory buildDirectory = MockDirectory();
+  final MockDirectory dartToolDirectory = MockDirectory();
+  final MockFile pubspec = MockFile();
+  final MockFile examplePubspec = MockFile();
+
+  when(mockFileSystem.currentDirectory).thenReturn(currentDirectory);
+  when(currentDirectory.childDirectory('example')).thenReturn(exampleDirectory);
+  when(currentDirectory.childFile('pubspec.yaml')).thenReturn(pubspec);
+  when(pubspec.path).thenReturn('/test/pubspec.yaml');
+  when(exampleDirectory.childFile('pubspec.yaml')).thenReturn(examplePubspec);
+  when(currentDirectory.childDirectory('.dart_tool')).thenReturn(dartToolDirectory);
+  when(examplePubspec.path).thenReturn('/test/example/pubspec.yaml');
+  when(mockFileSystem.isFileSync('/test/pubspec.yaml')).thenReturn(false);
+  when(mockFileSystem.isFileSync('/test/example/pubspec.yaml')).thenReturn(false);
+  when(mockFileSystem.directory('build')).thenReturn(buildDirectory);
+  when(mockFileSystem.path).thenReturn(fs.path);
+  when(buildDirectory.existsSync()).thenReturn(true);
+  when(dartToolDirectory.existsSync()).thenReturn(true);
+  group(CleanCommand, () {
+    testUsingContext('removes build and .dart_tool directories', () async {
+      await CleanCommand().runCommand();
+      verify(buildDirectory.deleteSync(recursive: true)).called(1);
+      verify(dartToolDirectory.deleteSync(recursive: true)).called(1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => mockFileSystem,
+      Config: () => null,
+    });
+  });
+}
+
+class MockFileSystem extends Mock implements FileSystem {}
+class MockFile extends Mock implements File {}
+class MockDirectory extends Mock implements Directory {}
diff --git a/packages/flutter_tools/test/general.shard/commands/config_test.dart b/packages/flutter_tools/test/general.shard/commands/config_test.dart
new file mode 100644
index 0000000..b95b4ed
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/config_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/android/android_studio.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/commands/config.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  MockAndroidStudio mockAndroidStudio;
+  MockAndroidSdk mockAndroidSdk;
+
+  setUp(() {
+    mockAndroidStudio = MockAndroidStudio();
+    mockAndroidSdk = MockAndroidSdk();
+  });
+
+  group('config', () {
+    testUsingContext('machine flag', () async {
+      final BufferLogger logger = context.get<Logger>();
+      final ConfigCommand command = ConfigCommand();
+      await command.handleMachine();
+
+      expect(logger.statusText, isNotEmpty);
+      final dynamic jsonObject = json.decode(logger.statusText);
+      expect(jsonObject, isMap);
+
+      expect(jsonObject.containsKey('android-studio-dir'), true);
+      expect(jsonObject['android-studio-dir'], isNotNull);
+
+      expect(jsonObject.containsKey('android-sdk'), true);
+      expect(jsonObject['android-sdk'], isNotNull);
+    }, overrides: <Type, Generator>{
+      AndroidStudio: () => mockAndroidStudio,
+      AndroidSdk: () => mockAndroidSdk,
+    });
+  });
+}
+
+class MockAndroidStudio extends Mock implements AndroidStudio, Comparable<AndroidStudio> {
+  @override
+  String get directory => 'path/to/android/stdio';
+}
+
+class MockAndroidSdk extends Mock implements AndroidSdk {
+  @override
+  String get directory => 'path/to/android/sdk';
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/create_test.dart b/packages/flutter_tools/test/general.shard/commands/create_test.dart
new file mode 100644
index 0000000..7a8c087
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/create_test.dart
@@ -0,0 +1,1266 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This test performs too poorly to run with coverage enabled.
+@Tags(<String>['create', 'no_coverage'])
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/net.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/create.dart';
+import 'package:flutter_tools/src/dart/sdk.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/version.dart';
+
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+
+const String frameworkRevision = '12345678';
+const String frameworkChannel = 'omega';
+final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
+  Platform: _kNoColorTerminalPlatform,
+};
+const String samplesIndexJson = '''[
+  { "id": "sample1" },
+  { "id": "sample2" }
+]''';
+
+void main() {
+  Directory tempDir;
+  Directory projectDir;
+  FlutterVersion mockFlutterVersion;
+  LoggingProcessManager loggingProcessManager;
+
+  setUpAll(() {
+    Cache.disableLocking();
+  });
+
+  setUp(() {
+    loggingProcessManager = LoggingProcessManager();
+    tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
+    projectDir = tempDir.childDirectory('flutter_project');
+    mockFlutterVersion = MockFlutterVersion();
+  });
+
+  tearDown(() {
+    tryToDelete(tempDir);
+  });
+
+  // Verify that we create a default project ('app') that is
+  // well-formed.
+  testUsingContext('can create a default project', () async {
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>[],
+      <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+        'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+        'flutter_project.iml',
+        'ios/Flutter/AppFrameworkInfo.plist',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/GeneratedPluginRegistrant.h',
+        'lib/main.dart',
+      ],
+    );
+    return _runFlutterTest(projectDir);
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('can create a default project if empty directory exists', () async {
+    await projectDir.create(recursive: true);
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>[],
+      <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+        'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+        'flutter_project.iml',
+        'ios/Flutter/AppFrameworkInfo.plist',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/GeneratedPluginRegistrant.h',
+      ],
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('creates a module project correctly', () async {
+    await _createAndAnalyzeProject(projectDir, <String>[
+      '--template=module',
+    ], <String>[
+      '.android/app/',
+      '.gitignore',
+      '.ios/Flutter',
+      '.metadata',
+      'lib/main.dart',
+      'pubspec.yaml',
+      'README.md',
+      'test/widget_test.dart',
+    ], unexpectedPaths: <String>[
+      'android/',
+      'ios/',
+    ]);
+    return _runFlutterTest(projectDir);
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('cannot create a project if non-empty non-project directory exists with .metadata', () async {
+    await projectDir.absolute.childDirectory('blag').create(recursive: true);
+    await projectDir.absolute.childFile('.metadata').writeAsString('project_type: blag\n');
+    expect(
+        () async => await _createAndAnalyzeProject(projectDir, <String>[], <String>[], unexpectedPaths: <String>[
+              'android/',
+              'ios/',
+              '.android/',
+              '.ios/',
+            ]),
+        throwsToolExit(message: 'Sorry, unable to detect the type of project to recreate'));
+  }, timeout: allowForRemotePubInvocation, overrides: noColorTerminalOverride);
+
+  testUsingContext('Will create an app project if non-empty non-project directory exists without .metadata', () async {
+    await projectDir.absolute.childDirectory('blag').create(recursive: true);
+    await projectDir.absolute.childDirectory('.idea').create(recursive: true);
+    await _createAndAnalyzeProject(projectDir, <String>[], <String>[
+      'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+      'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+      'flutter_project.iml',
+      'ios/Flutter/AppFrameworkInfo.plist',
+      'ios/Runner/AppDelegate.m',
+      'ios/Runner/GeneratedPluginRegistrant.h',
+    ], unexpectedPaths: <String>[
+      '.android/',
+      '.ios/',
+    ]);
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('detects and recreates an app project correctly', () async {
+    await projectDir.absolute.childDirectory('lib').create(recursive: true);
+    await projectDir.absolute.childDirectory('ios').create(recursive: true);
+    await _createAndAnalyzeProject(projectDir, <String>[], <String>[
+      'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+      'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+      'flutter_project.iml',
+      'ios/Flutter/AppFrameworkInfo.plist',
+      'ios/Runner/AppDelegate.m',
+      'ios/Runner/GeneratedPluginRegistrant.h',
+    ], unexpectedPaths: <String>[
+      '.android/',
+      '.ios/',
+    ]);
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('detects and recreates a plugin project correctly', () async {
+    await projectDir.create(recursive: true);
+    await projectDir.absolute.childFile('.metadata').writeAsString('project_type: plugin\n');
+    return _createAndAnalyzeProject(
+      projectDir,
+      <String>[],
+      <String>[
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'example/ios/Runner/AppDelegate.h',
+        'example/ios/Runner/AppDelegate.m',
+        'example/ios/Runner/main.m',
+        'example/lib/main.dart',
+        'flutter_project.iml',
+        'ios/Classes/FlutterProjectPlugin.h',
+        'ios/Classes/FlutterProjectPlugin.m',
+        'lib/flutter_project.dart',
+      ],
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('detects and recreates a package project correctly', () async {
+    await projectDir.create(recursive: true);
+    await projectDir.absolute.childFile('.metadata').writeAsString('project_type: package\n');
+    return _createAndAnalyzeProject(
+      projectDir,
+      <String>[],
+      <String>[
+        'lib/flutter_project.dart',
+        'test/flutter_project_test.dart',
+      ],
+      unexpectedPaths: <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'example/ios/Runner/AppDelegate.h',
+        'example/ios/Runner/AppDelegate.m',
+        'example/ios/Runner/main.m',
+        'example/lib/main.dart',
+        'ios/Classes/FlutterProjectPlugin.h',
+        'ios/Classes/FlutterProjectPlugin.m',
+        'ios/Runner/AppDelegate.h',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/main.m',
+        'lib/main.dart',
+        'test/widget_test.dart',
+      ],
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('kotlin/swift legacy app project', () async {
+    return _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=app', '--android-language=kotlin', '--ios-language=swift'],
+      <String>[
+        'android/app/src/main/kotlin/com/example/flutter_project/MainActivity.kt',
+        'ios/Runner/AppDelegate.swift',
+        'ios/Runner/Runner-Bridging-Header.h',
+        'lib/main.dart',
+        '.idea/libraries/KotlinJavaRuntime.xml',
+      ],
+      unexpectedPaths: <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+        'ios/Runner/AppDelegate.h',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/main.m',
+      ],
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can create a package project', () async {
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>['--template=package'],
+      <String>[
+        'lib/flutter_project.dart',
+        'test/flutter_project_test.dart',
+      ],
+      unexpectedPaths: <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'example/ios/Runner/AppDelegate.h',
+        'example/ios/Runner/AppDelegate.m',
+        'example/ios/Runner/main.m',
+        'example/lib/main.dart',
+        'ios/Classes/FlutterProjectPlugin.h',
+        'ios/Classes/FlutterProjectPlugin.m',
+        'ios/Runner/AppDelegate.h',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/main.m',
+        'lib/main.dart',
+        'test/widget_test.dart',
+      ],
+    );
+    return _runFlutterTest(projectDir);
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('can create a plugin project', () async {
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>['--template=plugin'],
+      <String>[
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'example/ios/Runner/AppDelegate.h',
+        'example/ios/Runner/AppDelegate.m',
+        'example/ios/Runner/main.m',
+        'example/lib/main.dart',
+        'flutter_project.iml',
+        'ios/Classes/FlutterProjectPlugin.h',
+        'ios/Classes/FlutterProjectPlugin.m',
+        'lib/flutter_project.dart',
+      ],
+    );
+    return _runFlutterTest(projectDir.childDirectory('example'));
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('kotlin/swift plugin project', () async {
+    return _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=plugin', '-a', 'kotlin', '--ios-language', 'swift'],
+      <String>[
+        'android/src/main/kotlin/com/example/flutter_project/FlutterProjectPlugin.kt',
+        'example/android/app/src/main/kotlin/com/example/flutter_project_example/MainActivity.kt',
+        'example/ios/Runner/AppDelegate.swift',
+        'example/ios/Runner/Runner-Bridging-Header.h',
+        'example/lib/main.dart',
+        'ios/Classes/FlutterProjectPlugin.h',
+        'ios/Classes/FlutterProjectPlugin.m',
+        'ios/Classes/SwiftFlutterProjectPlugin.swift',
+        'lib/flutter_project.dart',
+      ],
+      unexpectedPaths: <String>[
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'example/ios/Runner/AppDelegate.h',
+        'example/ios/Runner/AppDelegate.m',
+        'example/ios/Runner/main.m',
+      ],
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('plugin project with custom org', () async {
+    return _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=plugin', '--org', 'com.bar.foo'],
+      <String>[
+        'android/src/main/java/com/bar/foo/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java',
+      ],
+      unexpectedPaths: <String>[
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+      ],
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('plugin project with valid custom project name', () async {
+    return _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=plugin', '--project-name', 'xyz'],
+      <String>[
+        'android/src/main/java/com/example/xyz/XyzPlugin.java',
+        'example/android/app/src/main/java/com/example/xyz_example/MainActivity.java',
+      ],
+      unexpectedPaths: <String>[
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+      ],
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('plugin project with invalid custom project name', () async {
+    expect(
+      () => _createProject(projectDir,
+        <String>['--no-pub', '--template=plugin', '--project-name', 'xyz.xyz'],
+        <String>[],
+      ),
+      throwsToolExit(message: '"xyz.xyz" is not a valid Dart package name.'),
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('legacy app project with-driver-test', () async {
+    return _createAndAnalyzeProject(
+      projectDir,
+      <String>['--with-driver-test', '--template=app'],
+      <String>['lib/main.dart'],
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('module project with pub', () async {
+    return _createProject(projectDir, <String>[
+      '--template=module',
+    ], <String>[
+      '.android/build.gradle',
+      '.android/Flutter/build.gradle',
+      '.android/Flutter/src/main/AndroidManifest.xml',
+      '.android/Flutter/src/main/java/io/flutter/facade/Flutter.java',
+      '.android/Flutter/src/main/java/io/flutter/facade/FlutterFragment.java',
+      '.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+      '.android/gradle.properties',
+      '.android/gradle/wrapper/gradle-wrapper.jar',
+      '.android/gradle/wrapper/gradle-wrapper.properties',
+      '.android/gradlew',
+      '.android/gradlew.bat',
+      '.android/include_flutter.groovy',
+      '.android/local.properties',
+      '.android/settings.gradle',
+      '.gitignore',
+      '.metadata',
+      '.packages',
+      'lib/main.dart',
+      'pubspec.lock',
+      'pubspec.yaml',
+      'README.md',
+      'test/widget_test.dart',
+    ], unexpectedPaths: <String>[
+      'android/',
+      'ios/',
+    ]);
+  }, timeout: allowForRemotePubInvocation);
+
+
+  testUsingContext('androidx app project', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--androidx', projectDir.path]);
+
+    void expectExists(String relPath) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
+    }
+
+    expectExists('android/gradle.properties');
+
+    final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString();
+
+    expect(actualContents.contains('useAndroidX'), true);
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('non androidx app project', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--no-androidx', projectDir.path]);
+
+    void expectExists(String relPath) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
+    }
+
+    expectExists('android/gradle.properties');
+
+    final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString();
+
+    expect(actualContents.contains('useAndroidX'), false);
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('androidx app module', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--template=module', '--no-pub', '--androidx', projectDir.path]);
+
+    final FlutterProject project = FlutterProject.fromDirectory(projectDir);
+    expect(
+      project.usesAndroidX,
+      true,
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('non androidx app module', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--template=module', '--no-pub', '--no-androidx', projectDir.path]);
+
+    final FlutterProject project = FlutterProject.fromDirectory(projectDir);
+    expect(
+      project.usesAndroidX,
+      false,
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('androidx plugin project', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=plugin', '--androidx', projectDir.path]);
+
+    void expectExists(String relPath) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
+    }
+
+    expectExists('android/gradle.properties');
+
+    final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString();
+
+    expect(actualContents.contains('useAndroidX'), true);
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('non androidx plugin project', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=plugin', '--no-androidx', projectDir.path]);
+
+    void expectExists(String relPath) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
+    }
+
+    expectExists('android/gradle.properties');
+
+    final String actualContents = await fs.file(projectDir.path + '/android/gradle.properties').readAsString();
+
+    expect(actualContents.contains('useAndroidX'), false);
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('has correct content and formatting with module template', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--template=module', '--no-pub', '--org', 'com.foo.bar', projectDir.path]);
+
+    void expectExists(String relPath, [bool expectation = true]) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), expectation);
+    }
+
+    expectExists('lib/main.dart');
+    expectExists('test/widget_test.dart');
+
+    final String actualContents = await fs.file(projectDir.path + '/test/widget_test.dart').readAsString();
+
+    expect(actualContents.contains('flutter_test.dart'), true);
+
+    for (FileSystemEntity file in projectDir.listSync(recursive: true)) {
+      if (file is File && file.path.endsWith('.dart')) {
+        final String original = file.readAsStringSync();
+
+        final Process process = await Process.start(
+          sdkBinaryName('dartfmt'),
+          <String>[file.path],
+          workingDirectory: projectDir.path,
+        );
+        final String formatted = await process.stdout.transform(utf8.decoder).join();
+
+        expect(original, formatted, reason: file.path);
+      }
+    }
+
+    await _runFlutterTest(projectDir, target: fs.path.join(projectDir.path, 'test', 'widget_test.dart'));
+
+    // Generated Xcode settings
+    final String xcodeConfigPath = fs.path.join('.ios', 'Flutter', 'Generated.xcconfig');
+    expectExists(xcodeConfigPath);
+    final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath));
+    final String xcodeConfig = xcodeConfigFile.readAsStringSync();
+    expect(xcodeConfig, contains('FLUTTER_ROOT='));
+    expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH='));
+    expect(xcodeConfig, contains('FLUTTER_TARGET='));
+    // App identification
+    final String xcodeProjectPath = fs.path.join('.ios', 'Runner.xcodeproj', 'project.pbxproj');
+    expectExists(xcodeProjectPath);
+    final File xcodeProjectFile = fs.file(fs.path.join(projectDir.path, xcodeProjectPath));
+    final String xcodeProject = xcodeProjectFile.readAsStringSync();
+    expect(xcodeProject, contains('PRODUCT_BUNDLE_IDENTIFIER = com.foo.bar.flutterProject'));
+    // Xcode build system
+    final String xcodeWorkspaceSettingsPath = fs.path.join('.ios', 'Runner.xcworkspace', 'xcshareddata', 'WorkspaceSettings.xcsettings');
+    expectExists(xcodeWorkspaceSettingsPath, false);
+
+    final String versionPath = fs.path.join('.metadata');
+    expectExists(versionPath);
+    final String version = fs.file(fs.path.join(projectDir.path, versionPath)).readAsStringSync();
+    expect(version, contains('version:'));
+    expect(version, contains('revision: 12345678'));
+    expect(version, contains('channel: omega'));
+
+    // IntelliJ metadata
+    final String intelliJSdkMetadataPath = fs.path.join('.idea', 'libraries', 'Dart_SDK.xml');
+    expectExists(intelliJSdkMetadataPath);
+    final String sdkMetaContents = fs
+        .file(fs.path.join(
+          projectDir.path,
+          intelliJSdkMetadataPath,
+        ))
+        .readAsStringSync();
+    expect(sdkMetaContents, contains('<root url="file:/'));
+    expect(sdkMetaContents, contains('/bin/cache/dart-sdk/lib/core"'));
+  }, overrides: <Type, Generator>{
+    FlutterVersion: () => mockFlutterVersion,
+    Platform: _kNoColorTerminalPlatform,
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('has correct content and formatting with app template', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.foo.bar', projectDir.path]);
+
+    void expectExists(String relPath) {
+      expect(fs.isFileSync('${projectDir.path}/$relPath'), true);
+    }
+
+    expectExists('lib/main.dart');
+    expectExists('test/widget_test.dart');
+
+    for (FileSystemEntity file in projectDir.listSync(recursive: true)) {
+      if (file is File && file.path.endsWith('.dart')) {
+        final String original = file.readAsStringSync();
+
+        final Process process = await Process.start(
+          sdkBinaryName('dartfmt'),
+          <String>[file.path],
+          workingDirectory: projectDir.path,
+        );
+        final String formatted = await process.stdout.transform(utf8.decoder).join();
+
+        expect(original, formatted, reason: file.path);
+      }
+    }
+
+    await _runFlutterTest(projectDir, target: fs.path.join(projectDir.path, 'test', 'widget_test.dart'));
+
+    // Generated Xcode settings
+    final String xcodeConfigPath = fs.path.join('ios', 'Flutter', 'Generated.xcconfig');
+    expectExists(xcodeConfigPath);
+    final File xcodeConfigFile = fs.file(fs.path.join(projectDir.path, xcodeConfigPath));
+    final String xcodeConfig = xcodeConfigFile.readAsStringSync();
+    expect(xcodeConfig, contains('FLUTTER_ROOT='));
+    expect(xcodeConfig, contains('FLUTTER_APPLICATION_PATH='));
+    // App identification
+    final String xcodeProjectPath = fs.path.join('ios', 'Runner.xcodeproj', 'project.pbxproj');
+    expectExists(xcodeProjectPath);
+    final File xcodeProjectFile = fs.file(fs.path.join(projectDir.path, xcodeProjectPath));
+    final String xcodeProject = xcodeProjectFile.readAsStringSync();
+    expect(xcodeProject, contains('PRODUCT_BUNDLE_IDENTIFIER = com.foo.bar.flutterProject'));
+
+    final String versionPath = fs.path.join('.metadata');
+    expectExists(versionPath);
+    final String version = fs.file(fs.path.join(projectDir.path, versionPath)).readAsStringSync();
+    expect(version, contains('version:'));
+    expect(version, contains('revision: 12345678'));
+    expect(version, contains('channel: omega'));
+
+    // IntelliJ metadata
+    final String intelliJSdkMetadataPath = fs.path.join('.idea', 'libraries', 'Dart_SDK.xml');
+    expectExists(intelliJSdkMetadataPath);
+    final String sdkMetaContents = fs
+        .file(fs.path.join(
+          projectDir.path,
+          intelliJSdkMetadataPath,
+        ))
+        .readAsStringSync();
+    expect(sdkMetaContents, contains('<root url="file:/'));
+    expect(sdkMetaContents, contains('/bin/cache/dart-sdk/lib/core"'));
+  }, overrides: <Type, Generator>{
+    FlutterVersion: () => mockFlutterVersion,
+    Platform: _kNoColorTerminalPlatform,
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('has correct application id for android and bundle id for ios', () async {
+    Cache.flutterRoot = '../..';
+    when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
+    when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    String tmpProjectDir = fs.path.join(tempDir.path, 'hello_flutter');
+    await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'com.example', tmpProjectDir]);
+    FlutterProject project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir));
+    expect(
+        project.ios.productBundleIdentifier,
+        'com.example.helloFlutter',
+    );
+    expect(
+        project.android.applicationId,
+        'com.example.hello_flutter',
+    );
+
+    tmpProjectDir = fs.path.join(tempDir.path, 'test_abc');
+    await runner.run(<String>['create', '--template=app', '--no-pub', '--org', 'abc^*.1#@', tmpProjectDir]);
+    project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir));
+    expect(
+        project.ios.productBundleIdentifier,
+        'abc.1.testAbc',
+    );
+    expect(
+        project.android.applicationId,
+        'abc.u1.test_abc',
+    );
+
+    tmpProjectDir = fs.path.join(tempDir.path, 'flutter_project');
+    await runner.run(<String>['create', '--template=app', '--no-pub', '--org', '#+^%', tmpProjectDir]);
+    project = FlutterProject.fromDirectory(fs.directory(tmpProjectDir));
+    expect(
+        project.ios.productBundleIdentifier,
+        'flutterProject.untitled',
+    );
+    expect(
+        project.android.applicationId,
+        'flutter_project.untitled',
+    );
+  }, overrides: <Type, Generator>{
+    FlutterVersion: () => mockFlutterVersion,
+    Platform: _kNoColorTerminalPlatform,
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen default template over existing project', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: app\n'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen default template over existing app project with no metadta and detect the type', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]);
+
+    // Remove the .metadata to simulate an older instantiation that didn't generate those.
+    fs.file(fs.path.join(projectDir.path, '.metadata')).deleteSync();
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: app\n'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen app template over existing app project and detect the type', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=app', projectDir.path]);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: app\n'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen template over existing module project and detect the type', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=module', projectDir.path]);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: module\n'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen default template over existing plugin project and detect the type', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: plugin'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen default template over existing package project and detect the type', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    await runner.run(<String>['create', '--no-pub', '--template=package', projectDir.path]);
+
+    await runner.run(<String>['create', '--no-pub', projectDir.path]);
+
+    final String metadata = fs.file(fs.path.join(projectDir.path, '.metadata')).readAsStringSync();
+    expect(metadata, contains('project_type: package'));
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen module .android/ folder, reusing custom org', () async {
+    await _createProject(
+      projectDir,
+      <String>['--template=module', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    projectDir.childDirectory('.android').deleteSync(recursive: true);
+    return _createProject(
+      projectDir,
+      <String>[],
+      <String>[
+        '.android/app/src/main/java/com/bar/foo/flutter_project/host/MainActivity.java',
+      ],
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('can re-gen module .ios/ folder, reusing custom org', () async {
+    await _createProject(
+      projectDir,
+      <String>['--template=module', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    projectDir.childDirectory('.ios').deleteSync(recursive: true);
+    await _createProject(projectDir, <String>[], <String>[]);
+    final FlutterProject project = FlutterProject.fromDirectory(projectDir);
+    expect(
+      project.ios.productBundleIdentifier,
+      'com.bar.foo.flutterProject',
+    );
+  }, timeout: allowForRemotePubInvocation);
+
+  testUsingContext('can re-gen app android/ folder, reusing custom org', () async {
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    projectDir.childDirectory('android').deleteSync(recursive: true);
+    return _createProject(
+      projectDir,
+      <String>['--no-pub'],
+      <String>[
+        'android/app/src/main/java/com/bar/foo/flutter_project/MainActivity.java',
+      ],
+      unexpectedPaths: <String>[
+        'android/app/src/main/java/com/example/flutter_project/MainActivity.java',
+      ],
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen app ios/ folder, reusing custom org', () async {
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    projectDir.childDirectory('ios').deleteSync(recursive: true);
+    await _createProject(projectDir, <String>['--no-pub'], <String>[]);
+    final FlutterProject project = FlutterProject.fromDirectory(projectDir);
+    expect(
+      project.ios.productBundleIdentifier,
+      'com.bar.foo.flutterProject',
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('can re-gen plugin ios/ and example/ folders, reusing custom org', () async {
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=plugin', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    projectDir.childDirectory('example').deleteSync(recursive: true);
+    projectDir.childDirectory('ios').deleteSync(recursive: true);
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=plugin'],
+      <String>[
+        'example/android/app/src/main/java/com/bar/foo/flutter_project_example/MainActivity.java',
+        'ios/Classes/FlutterProjectPlugin.h',
+      ],
+      unexpectedPaths: <String>[
+        'example/android/app/src/main/java/com/example/flutter_project_example/MainActivity.java',
+        'android/src/main/java/com/example/flutter_project/FlutterProjectPlugin.java',
+      ],
+    );
+    final FlutterProject project = FlutterProject.fromDirectory(projectDir);
+    expect(
+      project.example.ios.productBundleIdentifier,
+      'com.bar.foo.flutterProjectExample',
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  testUsingContext('fails to re-gen without specified org when org is ambiguous', () async {
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=app', '--org', 'com.bar.foo'],
+      <String>[],
+    );
+    fs.directory(fs.path.join(projectDir.path, 'ios')).deleteSync(recursive: true);
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--template=app', '--org', 'com.bar.baz'],
+      <String>[],
+    );
+    expect(
+      () => _createProject(projectDir, <String>[], <String>[]),
+      throwsToolExit(message: 'Ambiguous organization'),
+    );
+  }, timeout: allowForCreateFlutterProject);
+
+  // Verify that we help the user correct an option ordering issue
+  testUsingContext('produces sensible error message', () async {
+    Cache.flutterRoot = '../..';
+
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+
+    expect(
+      runner.run(<String>['create', projectDir.path, '--pub']),
+      throwsToolExit(exitCode: 2, message: 'Try moving --pub'),
+    );
+  });
+
+  testUsingContext('fails when file exists where output directory should be', () async {
+    Cache.flutterRoot = '../..';
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
+    if (!existingFile.existsSync()) {
+      existingFile.createSync(recursive: true);
+    }
+    expect(
+      runner.run(<String>['create', existingFile.path]),
+      throwsToolExit(message: 'existing file'),
+    );
+  });
+
+  testUsingContext('fails overwrite when file exists where output directory should be', () async {
+    Cache.flutterRoot = '../..';
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    final File existingFile = fs.file(fs.path.join(projectDir.path, 'bad'));
+    if (!existingFile.existsSync()) {
+      existingFile.createSync(recursive: true);
+    }
+    expect(
+      runner.run(<String>['create', '--overwrite', existingFile.path]),
+      throwsToolExit(message: 'existing file'),
+    );
+  });
+
+  testUsingContext('overwrites existing directory when requested', () async {
+    Cache.flutterRoot = '../..';
+    final Directory existingDirectory = fs.directory(fs.path.join(projectDir.path, 'bad'));
+    if (!existingDirectory.existsSync()) {
+      existingDirectory.createSync(recursive: true);
+    }
+    final File existingFile = fs.file(fs.path.join(existingDirectory.path, 'lib', 'main.dart'));
+    existingFile.createSync(recursive: true);
+    await _createProject(
+      fs.directory(existingDirectory.path),
+      <String>['--overwrite'],
+      <String>[
+        'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+        'lib/main.dart',
+        'ios/Flutter/AppFrameworkInfo.plist',
+        'ios/Runner/AppDelegate.m',
+        'ios/Runner/GeneratedPluginRegistrant.h',
+      ],
+    );
+  });
+
+  testUsingContext('fails when invalid package name', () async {
+    Cache.flutterRoot = '../..';
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    expect(
+      runner.run(<String>['create', fs.path.join(projectDir.path, 'invalidName')]),
+      throwsToolExit(message: '"invalidName" is not a valid Dart package name.'),
+    );
+  });
+
+  testUsingContext(
+    'invokes pub offline when requested',
+    () async {
+      Cache.flutterRoot = '../..';
+
+      final CreateCommand command = CreateCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      await runner.run(<String>['create', '--pub', '--offline', projectDir.path]);
+      expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub')));
+      expect(loggingProcessManager.commands.first, contains('--offline'));
+    },
+    timeout: allowForCreateFlutterProject,
+    overrides: <Type, Generator>{
+      ProcessManager: () => loggingProcessManager,
+    },
+  );
+
+  testUsingContext(
+    'invokes pub online when offline not requested',
+    () async {
+      Cache.flutterRoot = '../..';
+
+      final CreateCommand command = CreateCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      await runner.run(<String>['create', '--pub', projectDir.path]);
+      expect(loggingProcessManager.commands.first, contains(matches(r'dart-sdk[\\/]bin[\\/]pub')));
+      expect(loggingProcessManager.commands.first, isNot(contains('--offline')));
+    },
+    timeout: allowForCreateFlutterProject,
+    overrides: <Type, Generator>{
+      ProcessManager: () => loggingProcessManager,
+    },
+  );
+
+  testUsingContext('can create a sample-based project', () async {
+    await _createAndAnalyzeProject(
+      projectDir,
+      <String>['--no-pub', '--sample=foo.bar.Baz'],
+      <String>[
+        'lib/main.dart',
+        'flutter_project.iml',
+        'android/app/src/main/AndroidManifest.xml',
+        'ios/Flutter/AppFrameworkInfo.plist',
+      ],
+      unexpectedPaths: <String>['test'],
+    );
+    expect(projectDir.childDirectory('lib').childFile('main.dart').readAsStringSync(),
+      contains('void main() {}'));
+  }, timeout: allowForRemotePubInvocation, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => MockHttpClient(200, result: 'void main() {}'),
+  });
+
+  testUsingContext('can write samples index to disk', () async {
+    final String outputFile = fs.path.join(tempDir.path, 'flutter_samples.json');
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    final List<String> args = <String>[
+      'create',
+      '--list-samples',
+      outputFile,
+    ];
+
+    await runner.run(args);
+    final File expectedFile = fs.file(outputFile);
+    expect(expectedFile.existsSync(), isTrue);
+    expect(expectedFile.readAsStringSync(), equals(samplesIndexJson));
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () =>
+        () => MockHttpClient(200, result: samplesIndexJson),
+  });
+  testUsingContext('provides an error to the user if samples json download fails', () async {
+    final String outputFile = fs.path.join(tempDir.path, 'flutter_samples.json');
+    final CreateCommand command = CreateCommand();
+    final CommandRunner<void> runner = createTestCommandRunner(command);
+    final List<String> args = <String>[
+      'create',
+      '--list-samples',
+      outputFile,
+    ];
+
+    await expectLater(runner.run(args), throwsToolExit(exitCode: 2, message: 'Failed to write samples'));
+    expect(fs.file(outputFile).existsSync(), isFalse);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () =>
+        () => MockHttpClient(404, result: 'not found'),
+  });
+}
+
+
+Future<void> _createProject(
+  Directory dir,
+  List<String> createArgs,
+  List<String> expectedPaths, {
+  List<String> unexpectedPaths = const <String>[],
+}) async {
+  Cache.flutterRoot = '../../..';
+  final CreateCommand command = CreateCommand();
+  final CommandRunner<void> runner = createTestCommandRunner(command);
+  await runner.run(<String>[
+    'create',
+    ...createArgs,
+    dir.path,
+  ]);
+
+  bool pathExists(String path) {
+    final String fullPath = fs.path.join(dir.path, path);
+    return fs.typeSync(fullPath) != FileSystemEntityType.notFound;
+  }
+
+  final List<String> failures = <String>[];
+  for (String path in expectedPaths) {
+    if (!pathExists(path)) {
+      failures.add('Path "$path" does not exist.');
+    }
+  }
+  for (String path in unexpectedPaths) {
+    if (pathExists(path)) {
+      failures.add('Path "$path" exists when it shouldn\'t.');
+    }
+  }
+  expect(failures, isEmpty, reason: failures.join('\n'));
+}
+
+Future<void> _createAndAnalyzeProject(
+  Directory dir,
+  List<String> createArgs,
+  List<String> expectedPaths, {
+  List<String> unexpectedPaths = const <String>[],
+}) async {
+  await _createProject(dir, createArgs, expectedPaths, unexpectedPaths: unexpectedPaths);
+  await _analyzeProject(dir.path);
+}
+
+Future<void> _analyzeProject(String workingDir) async {
+  final String flutterToolsPath = fs.path.absolute(fs.path.join(
+    'bin',
+    'flutter_tools.dart',
+  ));
+
+  final List<String> args = <String>[
+    ...dartVmFlags,
+    flutterToolsPath,
+    'analyze',
+  ];
+
+  final ProcessResult exec = await Process.run(
+    '$dartSdkPath/bin/dart',
+    args,
+    workingDirectory: workingDir,
+  );
+  if (exec.exitCode != 0) {
+    print(exec.stdout);
+    print(exec.stderr);
+  }
+  expect(exec.exitCode, 0);
+}
+
+Future<void> _runFlutterTest(Directory workingDir, { String target }) async {
+  final String flutterToolsPath = fs.path.absolute(fs.path.join(
+    'bin',
+    'flutter_tools.dart',
+  ));
+
+  // While flutter test does get packages, it doesn't write version
+  // files anymore.
+  await Process.run(
+    '$dartSdkPath/bin/dart',
+    <String>[
+      ...dartVmFlags,
+      flutterToolsPath,
+      'packages',
+      'get',
+    ],
+    workingDirectory: workingDir.path,
+  );
+
+  final List<String> args = <String>[
+    ...dartVmFlags,
+    flutterToolsPath,
+    'test',
+    '--no-color',
+    if (target != null) target,
+  ];
+
+  final ProcessResult exec = await Process.run(
+    '$dartSdkPath/bin/dart',
+    args,
+    workingDirectory: workingDir.path,
+  );
+  if (exec.exitCode != 0) {
+    print(exec.stdout);
+    print(exec.stderr);
+  }
+  expect(exec.exitCode, 0);
+}
+
+class MockFlutterVersion extends Mock implements FlutterVersion {}
+
+/// A ProcessManager that invokes a real process manager, but keeps
+/// track of all commands sent to it.
+class LoggingProcessManager extends LocalProcessManager {
+  List<List<String>> commands = <List<String>>[];
+
+  @override
+  Future<Process> start(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    ProcessStartMode mode = ProcessStartMode.normal,
+  }) {
+    commands.add(command);
+    return super.start(
+      command,
+      workingDirectory: workingDirectory,
+      environment: environment,
+      includeParentEnvironment: includeParentEnvironment,
+      runInShell: runInShell,
+      mode: mode,
+    );
+  }
+}
+
+class MockHttpClient implements HttpClient {
+  MockHttpClient(this.statusCode, {this.result});
+
+  final int statusCode;
+  final String result;
+
+  @override
+  Future<HttpClientRequest> getUrl(Uri url) async {
+    return MockHttpClientRequest(statusCode, result: result);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClient - $invocation';
+  }
+}
+
+class MockHttpClientRequest implements HttpClientRequest {
+  MockHttpClientRequest(this.statusCode, {this.result});
+
+  final int statusCode;
+  final String result;
+
+  @override
+  Future<HttpClientResponse> close() async {
+    return MockHttpClientResponse(statusCode, result: result);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientRequest - $invocation';
+  }
+}
+
+class MockHttpClientResponse implements HttpClientResponse {
+  MockHttpClientResponse(this.statusCode, {this.result});
+
+  @override
+  final int statusCode;
+
+  final String result;
+
+  @override
+  String get reasonPhrase => '<reason phrase>';
+
+  @override
+  HttpClientResponseCompressionState get compressionState {
+    return HttpClientResponseCompressionState.decompressed;
+  }
+
+  @override
+  StreamSubscription<Uint8List> listen(
+    void onData(Uint8List event), {
+    Function onError,
+    void onDone(),
+    bool cancelOnError,
+  }) {
+    return Stream<Uint8List>.fromIterable(<Uint8List>[Uint8List.fromList(result.codeUnits)])
+      .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+  }
+
+  @override
+  Future<dynamic> forEach(void Function(Uint8List element) action) {
+    action(Uint8List.fromList(result.codeUnits));
+    return Future<void>.value();
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientResponse - $invocation';
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
new file mode 100644
index 0000000..735cee5
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/create_usage_test.dart
@@ -0,0 +1,104 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/create.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/usage.dart';
+
+import '../../src/common.dart';
+import '../../src/testbed.dart';
+
+
+void main() {
+  group('usageValues', () {
+    Testbed testbed;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        final List<String> paths = <String>[
+          fs.path.join('flutter', 'packages', 'flutter', 'pubspec.yaml'),
+          fs.path.join('flutter', 'packages', 'flutter_driver', 'pubspec.yaml'),
+          fs.path.join('flutter', 'packages', 'flutter_test', 'pubspec.yaml'),
+          fs.path.join('flutter', 'bin', 'cache', 'artifacts', 'gradle_wrapper', 'wrapper'),
+          fs.path.join('usr', 'local', 'bin', 'adb'),
+          fs.path.join('Android', 'platform-tools', 'foo'),
+        ];
+        for (String path in paths) {
+          fs.file(path).createSync(recursive: true);
+        }
+      }, overrides: <Type, Generator>{
+        DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
+      });
+    });
+
+    test('set template type as usage value', () => testbed.run(() async {
+      final CreateCommand command = CreateCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=module', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'module'));
+
+      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'app'));
+
+      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=package', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'package'));
+
+      await runner.run(<String>['create',  '--flutter-root=flutter', '--no-pub', '--template=plugin', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateProjectType, 'plugin'));
+    }));
+
+    test('set iOS host language type as usage value', () => testbed.run(() async {
+      final CreateCommand command = CreateCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'objc'));
+
+      await runner.run(<String>[
+        'create',
+        '--flutter-root=flutter',
+        '--no-pub',
+        '--template=app',
+        '--ios-language=swift',
+        'testy',
+      ]);
+      expect(await command.usageValues, containsPair(kCommandCreateIosLanguage, 'swift'));
+
+    }));
+
+    test('set Android host language type as usage value', () => testbed.run(() async {
+      final CreateCommand command = CreateCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      await runner.run(<String>['create', '--flutter-root=flutter', '--no-pub', '--template=app', 'testy']);
+      expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'java'));
+
+      await runner.run(<String>[
+        'create',
+        '--flutter-root=flutter',
+        '--no-pub',
+        '--template=app',
+        '--android-language=kotlin',
+        'testy',
+      ]);
+      expect(await command.usageValues, containsPair(kCommandCreateAndroidLanguage, 'kotlin'));
+    }));
+  });
+}
+
+class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
+  @override
+  List<DoctorValidator> get validators => <DoctorValidator>[];
+
+  @override
+  List<Workflow> get workflows => <Workflow>[];
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/daemon_test.dart b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart
new file mode 100644
index 0000000..54aebdb
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/daemon_test.dart
@@ -0,0 +1,323 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/android/android_workflow.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/commands/daemon.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  Daemon daemon;
+  NotifyingLogger notifyingLogger;
+
+  group('daemon', () {
+    setUp(() {
+      notifyingLogger = NotifyingLogger();
+    });
+
+    tearDown(() {
+      if (daemon != null)
+        return daemon.shutdown();
+      notifyingLogger.dispose();
+    });
+
+    testUsingContext('daemon.version command should succeed', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.version'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['result'], isNotEmpty);
+      expect(response['result'] is String, true);
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('printError should send daemon.logMessage event', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      printError('daemon.logMessage test');
+      final Map<String, dynamic> response = await responses.stream.firstWhere((Map<String, dynamic> map) {
+        return map['event'] == 'daemon.logMessage' && map['params']['level'] == 'error';
+      });
+      expect(response['id'], isNull);
+      expect(response['event'], 'daemon.logMessage');
+      final Map<String, String> logMessage = response['params'].cast<String, String>();
+      expect(logMessage['level'], 'error');
+      expect(logMessage['message'], 'daemon.logMessage test');
+      await responses.close();
+      await commands.close();
+    }, overrides: <Type, Generator>{
+      Logger: () => notifyingLogger,
+    });
+
+    testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
+      final StringBuffer buffer = StringBuffer();
+
+      await runZoned<Future<void>>(() async {
+        final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+        final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+        daemon = Daemon(
+          commands.stream,
+          responses.add,
+          notifyingLogger: notifyingLogger,
+          logToStdout: true,
+        );
+        printStatus('daemon.logMessage test');
+        // Service the event loop.
+        await Future<void>.value();
+      }, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
+        buffer.writeln(line);
+      }));
+
+      expect(buffer.toString().trim(), 'daemon.logMessage test');
+    }, overrides: <Type, Generator>{
+      Logger: () => notifyingLogger,
+    });
+
+    testUsingContext('daemon.shutdown command should stop daemon', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      commands.add(<String, dynamic>{'id': 0, 'method': 'daemon.shutdown'});
+      return daemon.onExit.then<void>((int code) async {
+        await commands.close();
+        expect(code, 0);
+      });
+    });
+
+    testUsingContext('app.restart without an appId should report an error', () async {
+      final DaemonCommand command = DaemonCommand();
+      applyMocksToCommand(command);
+
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        daemonCommand: command,
+        notifyingLogger: notifyingLogger,
+      );
+
+      commands.add(<String, dynamic>{'id': 0, 'method': 'app.restart'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['error'], contains('appId is required'));
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
+      final DaemonCommand command = DaemonCommand();
+      applyMocksToCommand(command);
+
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+          commands.stream,
+          responses.add,
+          daemonCommand: command,
+          notifyingLogger: notifyingLogger,
+      );
+
+      commands.add(<String, dynamic>{
+        'id': 0,
+        'method': 'app.callServiceExtension',
+        'params': <String, String>{
+          'methodName': 'ext.flutter.debugPaint',
+        },
+      });
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['error'], contains('appId is required'));
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('app.stop without appId should report an error', () async {
+      final DaemonCommand command = DaemonCommand();
+      applyMocksToCommand(command);
+
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        daemonCommand: command,
+        notifyingLogger: notifyingLogger,
+      );
+
+      commands.add(<String, dynamic>{'id': 0, 'method': 'app.stop'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['error'], contains('appId is required'));
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('device.getDevices should respond with list', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['result'], isList);
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('device.getDevices reports available devices', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery();
+      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
+      discoverer.addDevice(MockAndroidDevice());
+      commands.add(<String, dynamic>{'id': 0, 'method': 'device.getDevices'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      final dynamic result = response['result'];
+      expect(result, isList);
+      expect(result, isNotEmpty);
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('should send device.added event when device is discovered', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+          commands.stream,
+          responses.add,
+          notifyingLogger: notifyingLogger,
+      );
+
+      final MockPollingDeviceDiscovery discoverer = MockPollingDeviceDiscovery();
+      daemon.deviceDomain.addDeviceDiscoverer(discoverer);
+      discoverer.addDevice(MockAndroidDevice());
+
+      return await responses.stream.skipWhile(_isConnectedEvent).first.then<void>((Map<String, dynamic> response) async {
+        expect(response['event'], 'device.added');
+        expect(response['params'], isMap);
+
+        final Map<String, dynamic> params = response['params'];
+        expect(params['platform'], isNotEmpty); // the mock device has a platform of 'android-arm'
+
+        await responses.close();
+        await commands.close();
+      });
+    }, overrides: <Type, Generator>{
+      AndroidWorkflow: () => MockAndroidWorkflow(),
+      IOSWorkflow: () => MockIOSWorkflow(),
+      FuchsiaWorkflow: () => MockFuchsiaWorkflow(),
+    });
+
+    testUsingContext('emulator.launch without an emulatorId should report an error', () async {
+      final DaemonCommand command = DaemonCommand();
+      applyMocksToCommand(command);
+
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        daemonCommand: command,
+        notifyingLogger: notifyingLogger,
+      );
+
+      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.launch'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['error'], contains('emulatorId is required'));
+      await responses.close();
+      await commands.close();
+    });
+
+    testUsingContext('emulator.getEmulators should respond with list', () async {
+      final StreamController<Map<String, dynamic>> commands = StreamController<Map<String, dynamic>>();
+      final StreamController<Map<String, dynamic>> responses = StreamController<Map<String, dynamic>>();
+      daemon = Daemon(
+        commands.stream,
+        responses.add,
+        notifyingLogger: notifyingLogger,
+      );
+      commands.add(<String, dynamic>{'id': 0, 'method': 'emulator.getEmulators'});
+      final Map<String, dynamic> response = await responses.stream.firstWhere(_notEvent);
+      expect(response['id'], 0);
+      expect(response['result'], isList);
+      await responses.close();
+      await commands.close();
+    });
+  });
+
+  group('daemon serialization', () {
+    test('OperationResult', () {
+      expect(
+        jsonEncodeObject(OperationResult.ok),
+        '{"code":0,"message":""}',
+      );
+      expect(
+        jsonEncodeObject(OperationResult(1, 'foo')),
+        '{"code":1,"message":"foo"}',
+      );
+    });
+  });
+}
+
+bool _notEvent(Map<String, dynamic> map) => map['event'] == null;
+
+bool _isConnectedEvent(Map<String, dynamic> map) => map['event'] == 'daemon.connected';
+
+class MockFuchsiaWorkflow extends FuchsiaWorkflow {
+  MockFuchsiaWorkflow({ this.canListDevices = true });
+
+  @override
+  final bool canListDevices;
+}
+
+class MockAndroidWorkflow extends AndroidWorkflow {
+  MockAndroidWorkflow({ this.canListDevices = true });
+
+  @override
+  final bool canListDevices;
+}
+
+class MockIOSWorkflow extends IOSWorkflow {
+  MockIOSWorkflow({ this.canListDevices =true });
+
+  @override
+  final bool canListDevices;
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/devices_test.dart b/packages/flutter_tools/test/general.shard/commands/devices_test.dart
new file mode 100644
index 0000000..01bbb9a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/devices_test.dart
@@ -0,0 +1,71 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/devices.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('devices', () {
+    setUpAll(() {
+      Cache.disableLocking();
+      // TODO(jonahwilliams): adjust the individual tests so they do not
+      // depend on the host environment.
+      debugDisableWebAndDesktop = true;
+    });
+
+    testUsingContext('returns 0 when called', () async {
+      final DevicesCommand command = DevicesCommand();
+      await createTestCommandRunner(command).run(<String>['devices']);
+    });
+
+    testUsingContext('no error when no connected devices', () async {
+      final DevicesCommand command = DevicesCommand();
+      await createTestCommandRunner(command).run(<String>['devices']);
+      expect(testLogger.statusText, contains('No devices detected'));
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => null,
+      DeviceManager: () => DeviceManager(),
+      ProcessManager: () => MockProcessManager(),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {
+  @override
+  Future<ProcessResult> run(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) async {
+    return ProcessResult(0, 0, '', '');
+  }
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    return ProcessResult(0, 0, '', '');
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/doctor_test.dart b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
new file mode 100644
index 0000000..5aa2a89
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/doctor_test.dart
@@ -0,0 +1,831 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// 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:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/base/user_messages.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:flutter_tools/src/proxy_validator.dart';
+import 'package:flutter_tools/src/vscode/vscode.dart';
+import 'package:flutter_tools/src/vscode/vscode_validator.dart';
+import 'package:flutter_tools/src/usage.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+final Generator _kNoColorOutputPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
+  Platform: _kNoColorOutputPlatform,
+};
+
+void main() {
+  MockProcessManager mockProcessManager;
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+  });
+
+  group('doctor', () {
+    testUsingContext('intellij validator', () async {
+      const String installPath = '/path/to/intelliJ';
+      final ValidationResult result = await IntelliJValidatorTestTarget('Test', installPath).validate();
+      expect(result.type, ValidationType.partial);
+      expect(result.statusInfo, 'version test.test.test');
+      expect(result.messages, hasLength(4));
+
+      ValidationMessage message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('IntelliJ '));
+      expect(message.message, 'IntelliJ at $installPath');
+
+      message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
+      expect(message.message, 'Dart plugin version 162.2485');
+
+      message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
+      expect(message.message, contains('Flutter plugin version 0.1.3'));
+      expect(message.message, contains('recommended minimum version'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('vs code validator when both installed', () async {
+      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension.validate();
+      expect(result.type, ValidationType.installed);
+      expect(result.statusInfo, 'version 1.2.3');
+      expect(result.messages, hasLength(2));
+
+      ValidationMessage message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
+      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
+
+      message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
+      expect(message.message, 'Flutter extension version 4.5.6');
+      expect(message.isError, isFalse);
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('vs code validator when 64bit installed', () async {
+      expect(VsCodeValidatorTestTargets.installedWithExtension64bit.title, 'VS Code, 64-bit edition');
+      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithExtension64bit.validate();
+      expect(result.type, ValidationType.installed);
+      expect(result.statusInfo, 'version 1.2.3');
+      expect(result.messages, hasLength(2));
+
+      ValidationMessage message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
+      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
+
+      message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
+      expect(message.message, 'Flutter extension version 4.5.6');
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('vs code validator when extension missing', () async {
+      final ValidationResult result = await VsCodeValidatorTestTargets.installedWithoutExtension.validate();
+      expect(result.type, ValidationType.partial);
+      expect(result.statusInfo, 'version 1.2.3');
+      expect(result.messages, hasLength(2));
+
+      ValidationMessage message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('VS Code '));
+      expect(message.message, 'VS Code at ${VsCodeValidatorTestTargets.validInstall}');
+
+      message = result.messages
+          .firstWhere((ValidationMessage m) => m.message.startsWith('Flutter '));
+      expect(message.message, startsWith('Flutter extension not installed'));
+      expect(message.isError, isTrue);
+    }, overrides: noColorTerminalOverride);
+  });
+
+  group('proxy validator', () {
+    testUsingContext('does not show if HTTP_PROXY is not set', () {
+      expect(ProxyValidator.shouldShow, isFalse);
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()..environment = <String, String>{},
+    });
+
+    testUsingContext('does not show if HTTP_PROXY is only whitespace', () {
+      expect(ProxyValidator.shouldShow, isFalse);
+    }, overrides: <Type, Generator>{
+      Platform: () =>
+          FakePlatform()..environment = <String, String>{'HTTP_PROXY': ' '},
+    });
+
+    testUsingContext('shows when HTTP_PROXY is set', () {
+      expect(ProxyValidator.shouldShow, isTrue);
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{'HTTP_PROXY': 'fakeproxy.local'},
+    });
+
+    testUsingContext('shows when http_proxy is set', () {
+      expect(ProxyValidator.shouldShow, isTrue);
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{'http_proxy': 'fakeproxy.local'},
+    });
+
+    testUsingContext('reports success when NO_PROXY is configured correctly', () async {
+      final ValidationResult results = await ProxyValidator().validate();
+      final List<ValidationMessage> issues = results.messages
+          .where((ValidationMessage msg) => msg.isError || msg.isHint)
+          .toList();
+      expect(issues, hasLength(0));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{
+          'HTTP_PROXY': 'fakeproxy.local',
+          'NO_PROXY': 'localhost,127.0.0.1',
+        },
+    });
+
+    testUsingContext('reports success when no_proxy is configured correctly', () async {
+      final ValidationResult results = await ProxyValidator().validate();
+      final List<ValidationMessage> issues = results.messages
+          .where((ValidationMessage msg) => msg.isError || msg.isHint)
+          .toList();
+      expect(issues, hasLength(0));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{
+          'http_proxy': 'fakeproxy.local',
+          'no_proxy': 'localhost,127.0.0.1',
+        },
+    });
+
+    testUsingContext('reports issues when NO_PROXY is missing localhost', () async {
+      final ValidationResult results = await ProxyValidator().validate();
+      final List<ValidationMessage> issues = results.messages
+          .where((ValidationMessage msg) => msg.isError || msg.isHint)
+          .toList();
+      expect(issues, isNot(hasLength(0)));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{
+          'HTTP_PROXY': 'fakeproxy.local',
+          'NO_PROXY': '127.0.0.1',
+        },
+    });
+
+    testUsingContext('reports issues when NO_PROXY is missing 127.0.0.1', () async {
+      final ValidationResult results = await ProxyValidator().validate();
+      final List<ValidationMessage> issues = results.messages
+          .where((ValidationMessage msg) => msg.isError || msg.isHint)
+          .toList();
+      expect(issues, isNot(hasLength(0)));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform()
+        ..environment = <String, String>{
+          'HTTP_PROXY': 'fakeproxy.local',
+          'NO_PROXY': 'localhost',
+        },
+    });
+  });
+
+  group('doctor with overridden validators', () {
+    testUsingContext('validate non-verbose output format for run without issues', () async {
+      expect(await doctor.diagnose(verbose: false), isTrue);
+      expect(testLogger.statusText, equals(
+              'Doctor summary (to see all details, run flutter doctor -v):\n'
+              '[✓] Passing Validator (with statusInfo)\n'
+              '[✓] Another Passing Validator (with statusInfo)\n'
+              '[✓] Providing validators is fun (with statusInfo)\n'
+              '\n'
+              '• No issues found!\n'
+      ));
+    }, overrides: <Type, Generator>{
+      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
+      Platform: _kNoColorOutputPlatform,
+    });
+  });
+
+  group('doctor usage params', () {
+    Usage mockUsage;
+
+    setUp(() {
+      mockUsage = MockUsage();
+      when(mockUsage.isFirstRun).thenReturn(true);
+    });
+
+    testUsingContext('contains installed', () async {
+      await doctor.diagnose(verbose: false);
+
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured,
+        <dynamic>['installed', 'installed', 'installed'],
+      );
+    }, overrides: <Type, Generator>{
+      DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
+      Platform: _kNoColorOutputPlatform,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('contains installed and partial', () async {
+      await FakePassingDoctor().diagnose(verbose: false);
+
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured,
+        <dynamic>['installed', 'installed'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithHintsOnly', captureAny)).captured,
+        <dynamic>['partial'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithErrors', captureAny)).captured,
+        <dynamic>['partial'],
+      );
+    }, overrides: <Type, Generator>{
+      Platform: _kNoColorOutputPlatform,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('contains installed, missing and partial', () async {
+      await FakeDoctor().diagnose(verbose: false);
+
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PassingValidator', captureAny)).captured,
+        <dynamic>['installed'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.MissingValidator', captureAny)).captured,
+        <dynamic>['missing'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.NotAvailableValidator', captureAny)).captured,
+        <dynamic>['notAvailable'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithHintsOnly', captureAny)).captured,
+        <dynamic>['partial'],
+      );
+      expect(
+        verify(mockUsage.sendEvent('doctorResult.PartialValidatorWithErrors', captureAny)).captured,
+        <dynamic>['partial'],
+      );
+    }, overrides: <Type, Generator>{
+      Platform: _kNoColorOutputPlatform,
+      Usage: () => mockUsage,
+    });
+  });
+
+  group('doctor with fake validators', () {
+    testUsingContext('validate non-verbose output format for run without issues', () async {
+      expect(await FakeQuietDoctor().diagnose(verbose: false), isTrue);
+      expect(testLogger.statusText, equals(
+              'Doctor summary (to see all details, run flutter doctor -v):\n'
+              '[✓] Passing Validator (with statusInfo)\n'
+              '[✓] Another Passing Validator (with statusInfo)\n'
+              '[✓] Validators are fun (with statusInfo)\n'
+              '[✓] Four score and seven validators ago (with statusInfo)\n'
+              '\n'
+              '• No issues found!\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate non-verbose output format when only one category fails', () async {
+      expect(await FakeSinglePassingDoctor().diagnose(verbose: false), isTrue);
+      expect(testLogger.statusText, equals(
+              'Doctor summary (to see all details, run flutter doctor -v):\n'
+              '[!] Partial Validator with only a Hint\n'
+              '    ! There is a hint here\n'
+              '\n'
+              '! Doctor found issues in 1 category.\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate non-verbose output format for a passing run', () async {
+      expect(await FakePassingDoctor().diagnose(verbose: false), isTrue);
+      expect(testLogger.statusText, equals(
+              'Doctor summary (to see all details, run flutter doctor -v):\n'
+              '[✓] Passing Validator (with statusInfo)\n'
+              '[!] Partial Validator with only a Hint\n'
+              '    ! There is a hint here\n'
+              '[!] Partial Validator with Errors\n'
+              '    ✗ An error message indicating partial installation\n'
+              '    ! Maybe a hint will help the user\n'
+              '[✓] Another Passing Validator (with statusInfo)\n'
+              '\n'
+              '! Doctor found issues in 2 categories.\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate non-verbose output format', () async {
+      expect(await FakeDoctor().diagnose(verbose: false), isFalse);
+      expect(testLogger.statusText, equals(
+              'Doctor summary (to see all details, run flutter doctor -v):\n'
+              '[✓] Passing Validator (with statusInfo)\n'
+              '[✗] Missing Validator\n'
+              '    ✗ A useful error message\n'
+              '    ! A hint message\n'
+              '[!] Not Available Validator\n'
+              '    ✗ A useful error message\n'
+              '    ! A hint message\n'
+              '[!] Partial Validator with only a Hint\n'
+              '    ! There is a hint here\n'
+              '[!] Partial Validator with Errors\n'
+              '    ✗ An error message indicating partial installation\n'
+              '    ! Maybe a hint will help the user\n'
+              '\n'
+              '! Doctor found issues in 4 categories.\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate verbose output format', () async {
+      expect(await FakeDoctor().diagnose(verbose: true), isFalse);
+      expect(testLogger.statusText, equals(
+              '[✓] Passing Validator (with statusInfo)\n'
+              '    • A helpful message\n'
+              '    • A second, somewhat longer helpful message\n'
+              '\n'
+              '[✗] Missing Validator\n'
+              '    ✗ A useful error message\n'
+              '    • A message that is not an error\n'
+              '    ! A hint message\n'
+              '\n'
+              '[!] Not Available Validator\n'
+              '    ✗ A useful error message\n'
+              '    • A message that is not an error\n'
+              '    ! A hint message\n'
+              '\n'
+              '[!] Partial Validator with only a Hint\n'
+              '    ! There is a hint here\n'
+              '    • But there is no error\n'
+              '\n'
+              '[!] Partial Validator with Errors\n'
+              '    ✗ An error message indicating partial installation\n'
+              '    ! Maybe a hint will help the user\n'
+              '    • An extra message with some verbose details\n'
+              '\n'
+              '! Doctor found issues in 4 categories.\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('gen_snapshot does not work', () async {
+      when(mockProcessManager.runSync(
+        <String>[artifacts.getArtifactPath(Artifact.genSnapshot)],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenReturn(ProcessResult(101, 1, '', ''));
+
+      expect(await FlutterValidatorDoctor().diagnose(verbose: false), isTrue);
+      final List<String> statusLines = testLogger.statusText.split('\n');
+      for (String msg in userMessages.flutterBinariesDoNotRun.split('\n')) {
+        expect(statusLines, contains(contains(msg)));
+      }
+      if (platform.isLinux) {
+        for (String msg in userMessages.flutterBinariesLinuxRepairCommands.split('\n')) {
+          expect(statusLines, contains(contains(msg)));
+        }
+      }
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+      ProcessManager: () => mockProcessManager,
+      Platform: _kNoColorOutputPlatform,
+    });
+  });
+
+  testUsingContext('validate non-verbose output wrapping', () async {
+    expect(await FakeDoctor().diagnose(verbose: false), isFalse);
+    expect(testLogger.statusText, equals(
+        'Doctor summary (to see all\n'
+        'details, run flutter doctor\n'
+        '-v):\n'
+        '[✓] Passing Validator (with\n'
+        '    statusInfo)\n'
+        '[✗] Missing Validator\n'
+        '    ✗ A useful error message\n'
+        '    ! A hint message\n'
+        '[!] Not Available Validator\n'
+        '    ✗ A useful error message\n'
+        '    ! A hint message\n'
+        '[!] Partial Validator with\n'
+        '    only a Hint\n'
+        '    ! There is a hint here\n'
+        '[!] Partial Validator with\n'
+        '    Errors\n'
+        '    ✗ An error message\n'
+        '      indicating partial\n'
+        '      installation\n'
+        '    ! Maybe a hint will help\n'
+        '      the user\n'
+        '\n'
+        '! Doctor found issues in 4\n'
+        '  categories.\n'
+        ''
+    ));
+  }, overrides: <Type, Generator>{
+    OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30),
+    Platform: _kNoColorOutputPlatform,
+  });
+
+  testUsingContext('validate verbose output wrapping', () async {
+    expect(await FakeDoctor().diagnose(verbose: true), isFalse);
+    expect(testLogger.statusText, equals(
+        '[✓] Passing Validator (with\n'
+        '    statusInfo)\n'
+        '    • A helpful message\n'
+        '    • A second, somewhat\n'
+        '      longer helpful message\n'
+        '\n'
+        '[✗] Missing Validator\n'
+        '    ✗ A useful error message\n'
+        '    • A message that is not an\n'
+        '      error\n'
+        '    ! A hint message\n'
+        '\n'
+        '[!] Not Available Validator\n'
+        '    ✗ A useful error message\n'
+        '    • A message that is not an\n'
+        '      error\n'
+        '    ! A hint message\n'
+        '\n'
+        '[!] Partial Validator with\n'
+        '    only a Hint\n'
+        '    ! There is a hint here\n'
+        '    • But there is no error\n'
+        '\n'
+        '[!] Partial Validator with\n'
+        '    Errors\n'
+        '    ✗ An error message\n'
+        '      indicating partial\n'
+        '      installation\n'
+        '    ! Maybe a hint will help\n'
+        '      the user\n'
+        '    • An extra message with\n'
+        '      some verbose details\n'
+        '\n'
+        '! Doctor found issues in 4\n'
+        '  categories.\n'
+        ''
+    ));
+  }, overrides: <Type, Generator>{
+    OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30),
+    Platform: _kNoColorOutputPlatform,
+  });
+
+
+  group('doctor with grouped validators', () {
+    testUsingContext('validate diagnose combines validator output', () async {
+      expect(await FakeGroupedDoctor().diagnose(), isTrue);
+      expect(testLogger.statusText, equals(
+              '[✓] Category 1\n'
+              '    • A helpful message\n'
+              '    • A helpful message\n'
+              '\n'
+              '[!] Category 2\n'
+              '    • A helpful message\n'
+              '    ✗ A useful error message\n'
+              '\n'
+              '! Doctor found issues in 1 category.\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate merging assigns statusInfo and title', () async {
+      // There are two subvalidators. Only the second contains statusInfo.
+      expect(await FakeGroupedDoctorWithStatus().diagnose(), isTrue);
+      expect(testLogger.statusText, equals(
+              '[✓] First validator title (A status message)\n'
+              '    • A helpful message\n'
+              '    • A different message\n'
+              '\n'
+              '• No issues found!\n'
+      ));
+    }, overrides: noColorTerminalOverride);
+  });
+
+
+  group('grouped validator merging results', () {
+    final PassingGroupedValidator installed = PassingGroupedValidator('Category');
+    final PartialGroupedValidator partial = PartialGroupedValidator('Category');
+    final MissingGroupedValidator missing = MissingGroupedValidator('Category');
+
+    testUsingContext('validate installed + installed = installed', () async {
+      expect(await FakeSmallGroupDoctor(installed, installed).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[✓]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate installed + partial = partial', () async {
+      expect(await FakeSmallGroupDoctor(installed, partial).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate installed + missing = partial', () async {
+      expect(await FakeSmallGroupDoctor(installed, missing).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate partial + installed = partial', () async {
+      expect(await FakeSmallGroupDoctor(partial, installed).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate partial + partial = partial', () async {
+      expect(await FakeSmallGroupDoctor(partial, partial).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate partial + missing = partial', () async {
+      expect(await FakeSmallGroupDoctor(partial, missing).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate missing + installed = partial', () async {
+      expect(await FakeSmallGroupDoctor(missing, installed).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate missing + partial = partial', () async {
+      expect(await FakeSmallGroupDoctor(missing, partial).diagnose(), isTrue);
+      expect(testLogger.statusText, startsWith('[!]'));
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('validate missing + missing = missing', () async {
+      expect(await FakeSmallGroupDoctor(missing, missing).diagnose(), isFalse);
+      expect(testLogger.statusText, startsWith('[✗]'));
+    }, overrides: noColorTerminalOverride);
+  });
+}
+
+class MockUsage extends Mock implements Usage {}
+
+class IntelliJValidatorTestTarget extends IntelliJValidator {
+  IntelliJValidatorTestTarget(String title, String installPath) : super(title, installPath);
+
+  @override
+  String get pluginsPath => fs.path.join('test', 'data', 'intellij', 'plugins');
+
+  @override
+  String get version => 'test.test.test';
+}
+
+class PassingValidator extends DoctorValidator {
+  PassingValidator(String name) : super(name);
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage('A helpful message'));
+    messages.add(ValidationMessage('A second, somewhat longer helpful message'));
+    return ValidationResult(ValidationType.installed, messages, statusInfo: 'with statusInfo');
+  }
+}
+
+class MissingValidator extends DoctorValidator {
+  MissingValidator() : super('Missing Validator');
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.error('A useful error message'));
+    messages.add(ValidationMessage('A message that is not an error'));
+    messages.add(ValidationMessage.hint('A hint message'));
+    return ValidationResult(ValidationType.missing, messages);
+  }
+}
+
+class NotAvailableValidator extends DoctorValidator {
+  NotAvailableValidator() : super('Not Available Validator');
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.error('A useful error message'));
+    messages.add(ValidationMessage('A message that is not an error'));
+    messages.add(ValidationMessage.hint('A hint message'));
+    return ValidationResult(ValidationType.notAvailable, messages);
+  }
+}
+
+class PartialValidatorWithErrors extends DoctorValidator {
+  PartialValidatorWithErrors() : super('Partial Validator with Errors');
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.error('An error message indicating partial installation'));
+    messages.add(ValidationMessage.hint('Maybe a hint will help the user'));
+    messages.add(ValidationMessage('An extra message with some verbose details'));
+    return ValidationResult(ValidationType.partial, messages);
+  }
+}
+
+class PartialValidatorWithHintsOnly extends DoctorValidator {
+  PartialValidatorWithHintsOnly() : super('Partial Validator with only a Hint');
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.hint('There is a hint here'));
+    messages.add(ValidationMessage('But there is no error'));
+    return ValidationResult(ValidationType.partial, messages);
+  }
+}
+
+/// A doctor that fails with a missing [ValidationResult].
+class FakeDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+
+  @override
+  List<DoctorValidator> get validators {
+    if (_validators == null) {
+      _validators = <DoctorValidator>[];
+      _validators.add(PassingValidator('Passing Validator'));
+      _validators.add(MissingValidator());
+      _validators.add(NotAvailableValidator());
+      _validators.add(PartialValidatorWithHintsOnly());
+      _validators.add(PartialValidatorWithErrors());
+    }
+    return _validators;
+  }
+}
+
+/// A doctor that should pass, but still has issues in some categories.
+class FakePassingDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    if (_validators == null) {
+      _validators = <DoctorValidator>[];
+      _validators.add(PassingValidator('Passing Validator'));
+      _validators.add(PartialValidatorWithHintsOnly());
+      _validators.add(PartialValidatorWithErrors());
+      _validators.add(PassingValidator('Another Passing Validator'));
+    }
+    return _validators;
+  }
+}
+
+/// A doctor that should pass, but still has 1 issue to test the singular of
+/// categories.
+class FakeSinglePassingDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    if (_validators == null) {
+      _validators = <DoctorValidator>[];
+      _validators.add(PartialValidatorWithHintsOnly());
+    }
+    return _validators;
+  }
+}
+
+/// A doctor that passes and has no issues anywhere.
+class FakeQuietDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    if (_validators == null) {
+      _validators = <DoctorValidator>[];
+      _validators.add(PassingValidator('Passing Validator'));
+      _validators.add(PassingValidator('Another Passing Validator'));
+      _validators.add(PassingValidator('Validators are fun'));
+      _validators.add(PassingValidator('Four score and seven validators ago'));
+    }
+    return _validators;
+  }
+}
+
+/// A DoctorValidatorsProvider that overrides the default validators without
+/// overriding the doctor.
+class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
+  @override
+  List<DoctorValidator> get validators {
+    return <DoctorValidator>[
+      PassingValidator('Passing Validator'),
+      PassingValidator('Another Passing Validator'),
+      PassingValidator('Providing validators is fun'),
+    ];
+  }
+
+  @override
+  List<Workflow> get workflows => <Workflow>[];
+}
+
+class PassingGroupedValidator extends DoctorValidator {
+  PassingGroupedValidator(String name) : super(name);
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage('A helpful message'));
+    return ValidationResult(ValidationType.installed, messages);
+  }
+}
+
+class MissingGroupedValidator extends DoctorValidator {
+  MissingGroupedValidator(String name) : super(name);
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.error('A useful error message'));
+    return ValidationResult(ValidationType.missing, messages);
+  }
+}
+
+class PartialGroupedValidator extends DoctorValidator {
+  PartialGroupedValidator(String name) : super(name);
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage.error('An error message for partial installation'));
+    return ValidationResult(ValidationType.partial, messages);
+  }
+}
+
+class PassingGroupedValidatorWithStatus extends DoctorValidator {
+  PassingGroupedValidatorWithStatus(String name) : super(name);
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    messages.add(ValidationMessage('A different message'));
+    return ValidationResult(ValidationType.installed, messages, statusInfo: 'A status message');
+  }
+}
+
+/// A doctor that has two groups of two validators each.
+class FakeGroupedDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    if (_validators == null) {
+      _validators = <DoctorValidator>[];
+      _validators.add(GroupedValidator(<DoctorValidator>[
+        PassingGroupedValidator('Category 1'),
+        PassingGroupedValidator('Category 1'),
+      ]));
+      _validators.add(GroupedValidator(<DoctorValidator>[
+        PassingGroupedValidator('Category 2'),
+        MissingGroupedValidator('Category 2'),
+      ]));
+    }
+    return _validators;
+  }
+}
+
+class FakeGroupedDoctorWithStatus extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    _validators ??= <DoctorValidator>[
+      GroupedValidator(<DoctorValidator>[
+        PassingGroupedValidator('First validator title'),
+        PassingGroupedValidatorWithStatus('Second validator title'),
+    ])];
+    return _validators;
+  }
+}
+
+class FlutterValidatorDoctor extends Doctor {
+  List<DoctorValidator> _validators;
+  @override
+  List<DoctorValidator> get validators {
+    _validators ??= <DoctorValidator>[FlutterValidator()];
+    return _validators;
+  }
+}
+
+/// A doctor that takes any two validators. Used to check behavior when
+/// merging ValidationTypes (installed, missing, partial).
+class FakeSmallGroupDoctor extends Doctor {
+  FakeSmallGroupDoctor(DoctorValidator val1, DoctorValidator val2) {
+    _validators = <DoctorValidator>[GroupedValidator(<DoctorValidator>[val1, val2])];
+  }
+
+  List<DoctorValidator> _validators;
+
+  @override
+  List<DoctorValidator> get validators => _validators;
+}
+
+class VsCodeValidatorTestTargets extends VsCodeValidator {
+  VsCodeValidatorTestTargets._(String installDirectory, String extensionDirectory, {String edition})
+    : super(VsCode.fromDirectory(installDirectory, extensionDirectory, edition: edition));
+
+  static VsCodeValidatorTestTargets get installedWithExtension =>
+      VsCodeValidatorTestTargets._(validInstall, validExtensions);
+
+  static VsCodeValidatorTestTargets get installedWithExtension64bit =>
+      VsCodeValidatorTestTargets._(validInstall, validExtensions, edition: '64-bit edition');
+
+  static VsCodeValidatorTestTargets get installedWithoutExtension =>
+      VsCodeValidatorTestTargets._(validInstall, missingExtensions);
+
+  static final String validInstall = fs.path.join('test', 'data', 'vscode', 'application');
+  static final String validExtensions = fs.path.join('test', 'data', 'vscode', 'extensions');
+  static final String missingExtensions = fs.path.join('test', 'data', 'vscode', 'notExtensions');
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/commands/drive_test.dart b/packages/flutter_tools/test/general.shard/commands/drive_test.dart
new file mode 100644
index 0000000..f6e39ed
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/drive_test.dart
@@ -0,0 +1,429 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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/android/android_device.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/drive.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('drive', () {
+    DriveCommand command;
+    Device mockDevice;
+    MemoryFileSystem fs;
+    Directory tempDir;
+
+    void withMockDevice([ Device mock ]) {
+      mockDevice = mock ?? MockDevice();
+      targetDeviceFinder = () async => mockDevice;
+      testDeviceManager.addDevice(mockDevice);
+    }
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      command = DriveCommand();
+      applyMocksToCommand(command);
+      fs = MemoryFileSystem();
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_drive_test.');
+      fs.currentDirectory = tempDir;
+      fs.directory('test').createSync();
+      fs.directory('test_driver').createSync();
+      fs.file('pubspec.yaml')..createSync();
+      fs.file('.packages').createSync();
+      setExitFunctionForTests();
+      targetDeviceFinder = () {
+        throw 'Unexpected call to targetDeviceFinder';
+      };
+      appStarter = (DriveCommand command) {
+        throw 'Unexpected call to appStarter';
+      };
+      testRunner = (List<String> testArgs, String observatoryUri) {
+        throw 'Unexpected call to testRunner';
+      };
+      appStopper = (DriveCommand command) {
+        throw 'Unexpected call to appStopper';
+      };
+    });
+
+    tearDown(() {
+      command = null;
+      restoreExitFunction();
+      restoreAppStarter();
+      restoreAppStopper();
+      restoreTestRunner();
+      restoreTargetDeviceFinder();
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('returns 1 when test file is not found', () async {
+      withMockDevice();
+
+      final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart');
+      final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
+      fs.file(testApp).createSync(recursive: true);
+
+      final List<String> args = <String>[
+        'drive',
+        '--target=$testApp',
+      ];
+      try {
+        await createTestCommandRunner(command).run(args);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 1);
+        expect(e.message, contains('Test file not found: $testFile'));
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns 1 when app fails to run', () async {
+      withMockDevice();
+      appStarter = expectAsync1((DriveCommand command) async => null);
+
+      final String testApp = fs.path.join(tempDir.path, 'test_driver', 'e2e.dart');
+      final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
+
+      final MemoryFileSystem memFs = fs;
+      await memFs.file(testApp).writeAsString('main() { }');
+      await memFs.file(testFile).writeAsString('main() { }');
+
+      final List<String> args = <String>[
+        'drive',
+        '--target=$testApp',
+      ];
+      try {
+        await createTestCommandRunner(command).run(args);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode, 1);
+        expect(e.message, contains('Application failed to start. Will not run test. Quitting.'));
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns 1 when app file is outside package', () async {
+      final String appFile = fs.path.join(tempDir.dirname, 'other_app', 'app.dart');
+      fs.file(appFile).createSync(recursive: true);
+      final List<String> args = <String>[
+        '--no-wrap',
+        'drive',
+        '--target=$appFile',
+      ];
+      try {
+        await createTestCommandRunner(command).run(args);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 1);
+        expect(testLogger.errorText, contains(
+            'Application file $appFile is outside the package directory ${tempDir.path}',
+        ));
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns 1 when app file is in the root dir', () async {
+      final String appFile = fs.path.join(tempDir.path, 'main.dart');
+      fs.file(appFile).createSync(recursive: true);
+      final List<String> args = <String>[
+        '--no-wrap',
+        'drive',
+        '--target=$appFile',
+      ];
+      try {
+        await createTestCommandRunner(command).run(args);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 1);
+        expect(testLogger.errorText, contains(
+            'Application file main.dart must reside in one of the '
+            'sub-directories of the package structure, not in the root directory.',
+        ));
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns 0 when test ends successfully', () async {
+      withMockDevice();
+
+      final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart');
+      final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
+
+      appStarter = expectAsync1((DriveCommand command) async {
+        return LaunchResult.succeeded();
+      });
+      testRunner = expectAsync2((List<String> testArgs, String observatoryUri) async {
+        expect(testArgs, <String>[testFile]);
+        return null;
+      });
+      appStopper = expectAsync1((DriveCommand command) async {
+        return true;
+      });
+
+      final MemoryFileSystem memFs = fs;
+      await memFs.file(testApp).writeAsString('main() {}');
+      await memFs.file(testFile).writeAsString('main() {}');
+
+      final List<String> args = <String>[
+        'drive',
+        '--target=$testApp',
+      ];
+      await createTestCommandRunner(command).run(args);
+      expect(testLogger.errorText, isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('returns exitCode set by test runner', () async {
+      withMockDevice();
+
+      final String testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart');
+      final String testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
+
+      appStarter = expectAsync1((DriveCommand command) async {
+        return LaunchResult.succeeded();
+      });
+      testRunner = (List<String> testArgs, String observatoryUri) async {
+        throwToolExit(null, exitCode: 123);
+      };
+      appStopper = expectAsync1((DriveCommand command) async {
+        return true;
+      });
+
+      final MemoryFileSystem memFs = fs;
+      await memFs.file(testApp).writeAsString('main() {}');
+      await memFs.file(testFile).writeAsString('main() {}');
+
+      final List<String> args = <String>[
+        'drive',
+        '--target=$testApp',
+      ];
+      try {
+        await createTestCommandRunner(command).run(args);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 123);
+        expect(e.message, isNull);
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    group('findTargetDevice', () {
+      testUsingContext('uses specified device', () async {
+        testDeviceManager.specifiedDeviceId = '123';
+        withMockDevice();
+        when(mockDevice.name).thenReturn('specified-device');
+        when(mockDevice.id).thenReturn('123');
+
+        final Device device = await findTargetDevice();
+        expect(device.name, 'specified-device');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+    });
+
+    void findTargetDeviceOnOperatingSystem(String operatingSystem) {
+      Platform platform() => FakePlatform(operatingSystem: operatingSystem);
+
+      testUsingContext('returns null if no devices found', () async {
+        expect(await findTargetDevice(), isNull);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: platform,
+      });
+
+      testUsingContext('uses existing Android device', () async {
+        mockDevice = MockAndroidDevice();
+        when(mockDevice.name).thenReturn('mock-android-device');
+        withMockDevice(mockDevice);
+
+        final Device device = await findTargetDevice();
+        expect(device.name, 'mock-android-device');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: platform,
+      });
+    }
+
+    group('findTargetDevice on Linux', () {
+      findTargetDeviceOnOperatingSystem('linux');
+    });
+
+    group('findTargetDevice on Windows', () {
+      findTargetDeviceOnOperatingSystem('windows');
+    });
+
+    group('findTargetDevice on macOS', () {
+      findTargetDeviceOnOperatingSystem('macos');
+
+      Platform macOsPlatform() => FakePlatform(operatingSystem: 'macos');
+
+      testUsingContext('uses existing simulator', () async {
+        withMockDevice();
+        when(mockDevice.name).thenReturn('mock-simulator');
+        when(mockDevice.isLocalEmulator)
+            .thenAnswer((Invocation invocation) => Future<bool>.value(true));
+
+        final Device device = await findTargetDevice();
+        expect(device.name, 'mock-simulator');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: macOsPlatform,
+      });
+    });
+
+    group('build arguments', () {
+      String testApp, testFile;
+
+      setUp(() {
+        restoreAppStarter();
+      });
+
+      Future<void> appStarterSetup() async {
+        withMockDevice();
+
+        final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader();
+        when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader);
+        final MockLaunchResult mockLaunchResult = MockLaunchResult();
+        when(mockLaunchResult.started).thenReturn(true);
+        when(mockDevice.startApp(
+            null,
+            mainPath: anyNamed('mainPath'),
+            route: anyNamed('route'),
+            debuggingOptions: anyNamed('debuggingOptions'),
+            platformArgs: anyNamed('platformArgs'),
+            prebuiltApplication: anyNamed('prebuiltApplication'),
+            usesTerminalUi: false,
+        )).thenAnswer((_) => Future<LaunchResult>.value(mockLaunchResult));
+        when(mockDevice.isAppInstalled(any)).thenAnswer((_) => Future<bool>.value(false));
+
+        testApp = fs.path.join(tempDir.path, 'test', 'e2e.dart');
+        testFile = fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart');
+
+        testRunner = (List<String> testArgs, String observatoryUri) async {
+          throwToolExit(null, exitCode: 123);
+        };
+        appStopper = expectAsync1(
+            (DriveCommand command) async {
+              return true;
+            },
+            count: 2,
+        );
+
+        final MemoryFileSystem memFs = fs;
+        await memFs.file(testApp).writeAsString('main() {}');
+        await memFs.file(testFile).writeAsString('main() {}');
+      }
+
+      testUsingContext('does not use pre-built app if no build arg provided', () async {
+        await appStarterSetup();
+
+        final List<String> args = <String>[
+          'drive',
+          '--target=$testApp',
+        ];
+        try {
+          await createTestCommandRunner(command).run(args);
+        } on ToolExit catch (e) {
+          expect(e.exitCode, 123);
+          expect(e.message, null);
+        }
+        verify(mockDevice.startApp(
+                null,
+                mainPath: anyNamed('mainPath'),
+                route: anyNamed('route'),
+                debuggingOptions: anyNamed('debuggingOptions'),
+                platformArgs: anyNamed('platformArgs'),
+                prebuiltApplication: false,
+                usesTerminalUi: false,
+        ));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+
+      testUsingContext('does not use pre-built app if --build arg provided', () async {
+        await appStarterSetup();
+
+        final List<String> args = <String>[
+          'drive',
+          '--build',
+          '--target=$testApp',
+        ];
+        try {
+          await createTestCommandRunner(command).run(args);
+        } on ToolExit catch (e) {
+          expect(e.exitCode, 123);
+          expect(e.message, null);
+        }
+        verify(mockDevice.startApp(
+                null,
+                mainPath: anyNamed('mainPath'),
+                route: anyNamed('route'),
+                debuggingOptions: anyNamed('debuggingOptions'),
+                platformArgs: anyNamed('platformArgs'),
+                prebuiltApplication: false,
+                usesTerminalUi: false,
+        ));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+
+      testUsingContext('uses prebuilt app if --no-build arg provided', () async {
+        await appStarterSetup();
+
+        final List<String> args = <String>[
+          'drive',
+          '--no-build',
+          '--target=$testApp',
+        ];
+        try {
+          await createTestCommandRunner(command).run(args);
+        } on ToolExit catch (e) {
+          expect(e.exitCode, 123);
+          expect(e.message, null);
+        }
+        verify(mockDevice.startApp(
+                null,
+                mainPath: anyNamed('mainPath'),
+                route: anyNamed('route'),
+                debuggingOptions: anyNamed('debuggingOptions'),
+                platformArgs: anyNamed('platformArgs'),
+                prebuiltApplication: true,
+                usesTerminalUi: false,
+        ));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+    });
+  });
+}
+
+class MockDevice extends Mock implements Device {
+  MockDevice() {
+    when(isSupported()).thenReturn(true);
+  }
+}
+
+class MockAndroidDevice extends Mock implements AndroidDevice { }
+
+class MockLaunchResult extends Mock implements LaunchResult { }
diff --git a/packages/flutter_tools/test/general.shard/commands/format_test.dart b/packages/flutter_tools/test/general.shard/commands/format_test.dart
new file mode 100644
index 0000000..1817bbe
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/format_test.dart
@@ -0,0 +1,78 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/format.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('format', () {
+    Directory tempDir;
+
+    setUp(() {
+      Cache.disableLocking();
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_format_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext('a file', () async {
+      final String projectPath = await createProject(tempDir);
+
+      final File srcFile = fs.file(fs.path.join(projectPath, 'lib', 'main.dart'));
+      final String original = srcFile.readAsStringSync();
+      srcFile.writeAsStringSync(original.replaceFirst('main()', 'main(  )'));
+
+      final FormatCommand command = FormatCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['format', srcFile.path]);
+
+      final String formatted = srcFile.readAsStringSync();
+      expect(formatted, original);
+    });
+
+    testUsingContext('dry-run', () async {
+      final String projectPath = await createProject(tempDir);
+
+      final File srcFile = fs.file(
+          fs.path.join(projectPath, 'lib', 'main.dart'));
+      final String nonFormatted = srcFile.readAsStringSync().replaceFirst(
+          'main()', 'main(  )');
+      srcFile.writeAsStringSync(nonFormatted);
+
+      final FormatCommand command = FormatCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>['format', '--dry-run', srcFile.path]);
+
+      final String shouldNotFormatted = srcFile.readAsStringSync();
+      expect(shouldNotFormatted, nonFormatted);
+    });
+
+    testUsingContext('dry-run with set-exit-if-changed', () async {
+      final String projectPath = await createProject(tempDir);
+
+      final File srcFile = fs.file(
+          fs.path.join(projectPath, 'lib', 'main.dart'));
+      final String nonFormatted = srcFile.readAsStringSync().replaceFirst(
+          'main()', 'main(  )');
+      srcFile.writeAsStringSync(nonFormatted);
+
+      final FormatCommand command = FormatCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+
+      expect(runner.run(<String>[
+        'format', '--dry-run', '--set-exit-if-changed', srcFile.path,
+      ]), throwsException);
+
+      final String shouldNotFormatted = srcFile.readAsStringSync();
+      expect(shouldNotFormatted, nonFormatted);
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart b/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart
new file mode 100644
index 0000000..dd6defa
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/ide_config_test.dart
@@ -0,0 +1,324 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/template.dart';
+import 'package:flutter_tools/src/commands/ide_config.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('ide_config', () {
+    Directory tempDir;
+    Directory templateDir;
+    Directory intellijDir;
+    Directory toolsDir;
+
+    Map<String, String> _getFilesystemContents([ Directory root ]) {
+      final String tempPath = tempDir.absolute.path;
+      final List<String> paths =
+        (root ?? tempDir).listSync(recursive: true).map((FileSystemEntity entity) {
+          final String relativePath = fs.path.relative(entity.path, from: tempPath);
+          return relativePath;
+        }).toList();
+      final Map<String, String> contents = <String, String>{};
+      for (String path in paths) {
+        final String absPath = fs.path.join(tempPath, path);
+        if (fs.isDirectorySync(absPath)) {
+          contents[path] = 'dir';
+        } else if (fs.isFileSync(absPath)) {
+          contents[path] = fs.file(absPath).readAsStringSync();
+        }
+      }
+      return contents;
+    }
+
+    Map<String, String> _getManifest(Directory base, String marker, { bool isTemplate = false }) {
+      final String basePath = fs.path.relative(base.path, from: tempDir.absolute.path);
+      final String suffix = isTemplate ? Template.copyTemplateExtension : '';
+      return <String, String>{
+        fs.path.join(basePath, '.idea'): 'dir',
+        fs.path.join(basePath, '.idea', 'modules.xml$suffix'): 'modules $marker',
+        fs.path.join(basePath, '.idea', 'vcs.xml$suffix'): 'vcs $marker',
+        fs.path.join(basePath, '.idea', '.name$suffix'):
+            'codeStyleSettings $marker',
+        fs.path.join(basePath, '.idea', 'runConfigurations'): 'dir',
+        fs.path.join(basePath, '.idea', 'runConfigurations', 'hello_world.xml$suffix'):
+            'hello_world $marker',
+        fs.path.join(basePath, 'flutter.iml$suffix'): 'flutter $marker',
+        fs.path.join(basePath, 'packages', 'new', 'deep.iml$suffix'): 'deep $marker',
+      };
+    }
+
+    void _populateDir(Map<String, String> manifest) {
+      for (String key in manifest.keys) {
+        if (manifest[key] == 'dir') {
+          tempDir.childDirectory(key)..createSync(recursive: true);
+        }
+      }
+      for (String key in manifest.keys) {
+        if (manifest[key] != 'dir') {
+          tempDir.childFile(key)
+            ..createSync(recursive: true)
+            ..writeAsStringSync(manifest[key]);
+        }
+      }
+    }
+
+    bool _fileOrDirectoryExists(String path) {
+      final String absPath = fs.path.join(tempDir.absolute.path, path);
+      return fs.file(absPath).existsSync() || fs.directory(absPath).existsSync();
+    }
+
+    Future<void> _updateIdeConfig({
+      Directory dir,
+      List<String> args = const <String>[],
+      Map<String, String> expectedContents = const <String, String>{},
+      List<String> unexpectedPaths = const <String>[],
+    }) async {
+      dir ??= tempDir;
+      final IdeConfigCommand command = IdeConfigCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>[
+        '--flutter-root=${tempDir.absolute.path}',
+        'ide-config',
+        ...args,
+      ]);
+
+      for (String path in expectedContents.keys) {
+        final String absPath = fs.path.join(tempDir.absolute.path, path);
+        expect(_fileOrDirectoryExists(fs.path.join(dir.path, path)), true,
+            reason: "$path doesn't exist");
+        if (fs.file(absPath).existsSync()) {
+          expect(fs.file(absPath).readAsStringSync(), equals(expectedContents[path]),
+              reason: "$path contents don't match");
+        }
+      }
+      for (String path in unexpectedPaths) {
+        expect(_fileOrDirectoryExists(fs.path.join(dir.path, path)), false, reason: '$path exists');
+      }
+    }
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_ide_config_test.');
+      final Directory packagesDir = tempDir.childDirectory('packages')..createSync(recursive: true);
+      toolsDir = packagesDir.childDirectory('flutter_tools')..createSync();
+      templateDir = toolsDir.childDirectory('ide_templates')..createSync();
+      intellijDir = templateDir.childDirectory('intellij')..createSync();
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    testUsingContext("doesn't touch existing files without --overwrite", () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      _populateDir(templateManifest);
+      _populateDir(flutterManifest);
+      final Map<String, String> expectedContents = _getFilesystemContents();
+      return _updateIdeConfig(
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('creates non-existent files', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'template',
+      );
+      _populateDir(templateManifest);
+      final Map<String, String> expectedContents = <String, String>{
+        ...templateManifest,
+        ...flutterManifest,
+      };
+      return _updateIdeConfig(
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('overwrites existing files with --overwrite', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      _populateDir(templateManifest);
+      _populateDir(flutterManifest);
+      final Map<String, String> overwrittenManifest = _getManifest(
+        tempDir,
+        'template',
+      );
+      final Map<String, String> expectedContents = <String, String>{
+        ...templateManifest,
+        ...overwrittenManifest,
+      };
+      return _updateIdeConfig(
+        args: <String>['--overwrite'],
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('only adds new templates without --overwrite', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      final String flutterIml = fs.path.join(
+        'packages',
+        'flutter_tools',
+        'ide_templates',
+        'intellij',
+        'flutter.iml${Template.copyTemplateExtension}',
+      );
+      templateManifest.remove(flutterIml);
+      _populateDir(templateManifest);
+      templateManifest[flutterIml] = 'flutter existing';
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      _populateDir(flutterManifest);
+      final Map<String, String> expectedContents = <String, String>{
+        ...flutterManifest,
+        ...templateManifest,
+      };
+      return _updateIdeConfig(
+        args: <String>['--update-templates'],
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('update all templates with --overwrite', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      _populateDir(templateManifest);
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      _populateDir(flutterManifest);
+      final Map<String, String> updatedTemplates = _getManifest(
+        intellijDir,
+        'existing',
+        isTemplate: true,
+      );
+      final Map<String, String> expectedContents = <String, String>{
+        ...flutterManifest,
+        ...updatedTemplates,
+      };
+      return _updateIdeConfig(
+        args: <String>['--update-templates', '--overwrite'],
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('removes deleted imls with --overwrite', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      _populateDir(templateManifest);
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      flutterManifest.remove('flutter.iml');
+      _populateDir(flutterManifest);
+      final Map<String, String> updatedTemplates = _getManifest(
+        intellijDir,
+        'existing',
+        isTemplate: true,
+      );
+      final String flutterIml = fs.path.join(
+        'packages',
+        'flutter_tools',
+        'ide_templates',
+        'intellij',
+        'flutter.iml${Template.copyTemplateExtension}',
+      );
+      updatedTemplates.remove(flutterIml);
+      final Map<String, String> expectedContents = <String, String>{
+        ...flutterManifest,
+        ...updatedTemplates,
+      };
+      return _updateIdeConfig(
+        args: <String>['--update-templates', '--overwrite'],
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+    testUsingContext('removes deleted imls with --overwrite, including empty parent dirs', () async {
+      final Map<String, String> templateManifest = _getManifest(
+        intellijDir,
+        'template',
+        isTemplate: true,
+      );
+      _populateDir(templateManifest);
+      final Map<String, String> flutterManifest = _getManifest(
+        tempDir,
+        'existing',
+      );
+      flutterManifest.remove(fs.path.join('packages', 'new', 'deep.iml'));
+      _populateDir(flutterManifest);
+      final Map<String, String> updatedTemplates = _getManifest(
+        intellijDir,
+        'existing',
+        isTemplate: true,
+      );
+      String deepIml = fs.path.join(
+        'packages',
+        'flutter_tools',
+        'ide_templates',
+        'intellij');
+      // Remove the all the dir entries too.
+      updatedTemplates.remove(deepIml);
+      deepIml = fs.path.join(deepIml, 'packages');
+      updatedTemplates.remove(deepIml);
+      deepIml = fs.path.join(deepIml, 'new');
+      updatedTemplates.remove(deepIml);
+      deepIml = fs.path.join(deepIml, 'deep.iml');
+      updatedTemplates.remove(deepIml);
+      final Map<String, String> expectedContents = <String, String>{
+        ...flutterManifest,
+        ...updatedTemplates,
+      };
+      return _updateIdeConfig(
+        args: <String>['--update-templates', '--overwrite'],
+        expectedContents: expectedContents,
+      );
+    }, timeout: const Timeout.factor(2.0));
+
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/install_test.dart b/packages/flutter_tools/test/general.shard/commands/install_test.dart
new file mode 100644
index 0000000..2883259
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/install_test.dart
@@ -0,0 +1,49 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/install.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('install', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    testUsingContext('returns 0 when Android is connected and ready for an install', () async {
+      final InstallCommand command = InstallCommand();
+      applyMocksToCommand(command);
+
+      final MockAndroidDevice device = MockAndroidDevice();
+      when(device.isAppInstalled(any)).thenAnswer((_) async => false);
+      when(device.installApp(any)).thenAnswer((_) async => true);
+      testDeviceManager.addDevice(device);
+
+      await createTestCommandRunner(command).run(<String>['install']);
+    }, overrides: <Type, Generator>{
+      Cache: () => MockCache(),
+    });
+
+    testUsingContext('returns 0 when iOS is connected and ready for an install', () async {
+      final InstallCommand command = InstallCommand();
+      applyMocksToCommand(command);
+
+      final MockIOSDevice device = MockIOSDevice();
+      when(device.isAppInstalled(any)).thenAnswer((_) async => false);
+      when(device.installApp(any)).thenAnswer((_) async => true);
+      testDeviceManager.addDevice(device);
+
+      await createTestCommandRunner(command).run(<String>['install']);
+    }, overrides: <Type, Generator>{
+      Cache: () => MockCache(),
+    });
+  });
+}
+
+class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/commands/packages_test.dart b/packages/flutter_tools/test/general.shard/commands/packages_test.dart
new file mode 100644
index 0000000..8e0269b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/packages_test.dart
@@ -0,0 +1,448 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:args/command_runner.dart';
+import 'package:flutter_tools/src/base/file_system.dart' hide IOSink;
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/utils.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/packages.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart' show MockProcessManager, MockStdio, PromptingProcess;
+
+class AlwaysTrueBotDetector implements BotDetector {
+  const AlwaysTrueBotDetector();
+
+  @override
+  bool get isRunningOnBot => true;
+}
+
+
+class AlwaysFalseBotDetector implements BotDetector {
+  const AlwaysFalseBotDetector();
+
+  @override
+  bool get isRunningOnBot => false;
+}
+
+
+void main() {
+  Cache.disableLocking();
+  group('packages get/upgrade', () {
+    Directory tempDir;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    Future<String> createProjectWithPlugin(String plugin, { List<String> arguments }) async {
+      final String projectPath = await createProject(tempDir, arguments: arguments);
+      final File pubspec = fs.file(fs.path.join(projectPath, 'pubspec.yaml'));
+      String content = await pubspec.readAsString();
+      content = content.replaceFirst(
+        '\ndependencies:\n',
+        '\ndependencies:\n  $plugin:\n',
+      );
+      await pubspec.writeAsString(content, flush: true);
+      return projectPath;
+    }
+
+    Future<PackagesCommand> runCommandIn(String projectPath, String verb, { List<String> args }) async {
+      final PackagesCommand command = PackagesCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>[
+        'packages',
+        verb,
+        ...?args,
+        projectPath,
+      ]);
+      return command;
+    }
+
+    void expectExists(String projectPath, String relPath) {
+      expect(
+        fs.isFileSync(fs.path.join(projectPath, relPath)),
+        true,
+        reason: '$projectPath/$relPath should exist, but does not',
+      );
+    }
+
+    void expectContains(String projectPath, String relPath, String substring) {
+      expectExists(projectPath, relPath);
+      expect(
+        fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(),
+        contains(substring),
+        reason: '$projectPath/$relPath has unexpected content',
+      );
+    }
+
+    void expectNotExists(String projectPath, String relPath) {
+      expect(
+        fs.isFileSync(fs.path.join(projectPath, relPath)),
+        false,
+        reason: '$projectPath/$relPath should not exist, but does',
+      );
+    }
+
+    void expectNotContains(String projectPath, String relPath, String substring) {
+      expectExists(projectPath, relPath);
+      expect(
+        fs.file(fs.path.join(projectPath, relPath)).readAsStringSync(),
+        isNot(contains(substring)),
+        reason: '$projectPath/$relPath has unexpected content',
+      );
+    }
+
+    const List<String> pubOutput = <String>[
+      '.packages',
+      'pubspec.lock',
+    ];
+
+    const List<String> pluginRegistrants = <String>[
+      'ios/Runner/GeneratedPluginRegistrant.h',
+      'ios/Runner/GeneratedPluginRegistrant.m',
+      'android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+    ];
+
+    const List<String> modulePluginRegistrants = <String>[
+      '.ios/Flutter/FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.h',
+      '.ios/Flutter/FlutterPluginRegistrant/Classes/GeneratedPluginRegistrant.m',
+      '.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+    ];
+
+    const List<String> pluginWitnesses = <String>[
+      '.flutter-plugins',
+      'ios/Podfile',
+    ];
+
+    const List<String> modulePluginWitnesses = <String>[
+      '.flutter-plugins',
+      '.ios/Podfile',
+    ];
+
+    const Map<String, String> pluginContentWitnesses = <String, String>{
+      'ios/Flutter/Debug.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"',
+      'ios/Flutter/Release.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"',
+    };
+
+    const Map<String, String> modulePluginContentWitnesses = <String, String>{
+      '.ios/Config/Debug.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"',
+      '.ios/Config/Release.xcconfig': '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"',
+    };
+
+    void expectDependenciesResolved(String projectPath) {
+      for (String output in pubOutput) {
+        expectExists(projectPath, output);
+      }
+    }
+
+    void expectZeroPluginsInjected(String projectPath) {
+      for (final String registrant in modulePluginRegistrants) {
+        expectExists(projectPath, registrant);
+      }
+      for (final String witness in pluginWitnesses) {
+        expectNotExists(projectPath, witness);
+      }
+      modulePluginContentWitnesses.forEach((String witness, String content) {
+        expectNotContains(projectPath, witness, content);
+      });
+    }
+
+    void expectPluginInjected(String projectPath) {
+      for (final String registrant in pluginRegistrants) {
+        expectExists(projectPath, registrant);
+      }
+      for (final String witness in pluginWitnesses) {
+        expectExists(projectPath, witness);
+      }
+      pluginContentWitnesses.forEach((String witness, String content) {
+        expectContains(projectPath, witness, content);
+      });
+    }
+
+    void expectModulePluginInjected(String projectPath) {
+      for (final String registrant in modulePluginRegistrants) {
+        expectExists(projectPath, registrant);
+      }
+      for (final String witness in modulePluginWitnesses) {
+        expectExists(projectPath, witness);
+      }
+      modulePluginContentWitnesses.forEach((String witness, String content) {
+        expectContains(projectPath, witness, content);
+      });
+    }
+
+    void removeGeneratedFiles(String projectPath) {
+      final Iterable<String> allFiles = <List<String>>[
+        pubOutput,
+        modulePluginRegistrants,
+        pluginWitnesses,
+      ].expand<String>((List<String> list) => list);
+      for (String path in allFiles) {
+        final File file = fs.file(fs.path.join(projectPath, path));
+        if (file.existsSync())
+          file.deleteSync();
+      }
+    }
+
+    testUsingContext('get fetches packages', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      await runCommandIn(projectPath, 'get');
+
+      expectDependenciesResolved(projectPath);
+      expectZeroPluginsInjected(projectPath);
+    }, timeout: allowForRemotePubInvocation);
+
+    testUsingContext('get --offline fetches packages', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      await runCommandIn(projectPath, 'get', args: <String>['--offline']);
+
+      expectDependenciesResolved(projectPath);
+      expectZeroPluginsInjected(projectPath);
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('set the number of plugins as usage value', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      final PackagesCommand command = await runCommandIn(projectPath, 'get');
+      final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
+
+      expect(await getCommand.usageValues, containsPair(kCommandPackagesNumberPlugins, '0'));
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate that the project is not a module in usage value', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub']);
+      removeGeneratedFiles(projectPath);
+
+      final PackagesCommand command = await runCommandIn(projectPath, 'get');
+      final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
+
+      expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'false'));
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate that the project is a module in usage value', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      final PackagesCommand command = await runCommandIn(projectPath, 'get');
+      final PackagesGetCommand getCommand = command.subcommands['get'] as PackagesGetCommand;
+
+      expect(await getCommand.usageValues, containsPair(kCommandPackagesProjectModule, 'true'));
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('upgrade fetches packages', () async {
+      final String projectPath = await createProject(tempDir,
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      await runCommandIn(projectPath, 'upgrade');
+
+      expectDependenciesResolved(projectPath);
+      expectZeroPluginsInjected(projectPath);
+    }, timeout: allowForRemotePubInvocation);
+
+    testUsingContext('get fetches packages and injects plugin', () async {
+      final String projectPath = await createProjectWithPlugin('path_provider',
+        arguments: <String>['--no-pub', '--template=module']);
+      removeGeneratedFiles(projectPath);
+
+      await runCommandIn(projectPath, 'get');
+
+      expectDependenciesResolved(projectPath);
+      expectModulePluginInjected(projectPath);
+    }, timeout: allowForRemotePubInvocation);
+
+    testUsingContext('get fetches packages and injects plugin in plugin project', () async {
+      final String projectPath = await createProject(
+        tempDir,
+        arguments: <String>['--template=plugin', '--no-pub'],
+      );
+      final String exampleProjectPath = fs.path.join(projectPath, 'example');
+      removeGeneratedFiles(projectPath);
+      removeGeneratedFiles(exampleProjectPath);
+
+      await runCommandIn(projectPath, 'get');
+
+      expectDependenciesResolved(projectPath);
+
+      await runCommandIn(exampleProjectPath, 'get');
+
+      expectDependenciesResolved(exampleProjectPath);
+      expectPluginInjected(exampleProjectPath);
+    }, timeout: allowForRemotePubInvocation);
+  });
+
+  group('packages test/pub', () {
+    MockProcessManager mockProcessManager;
+    MockStdio mockStdio;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockStdio = MockStdio();
+    });
+
+    testUsingContext('test without bot', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'test']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], 'run');
+      expect(commands[2], 'test');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysFalseBotDetector(),
+    });
+
+    testUsingContext('test with bot', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'test']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(4));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'run');
+      expect(commands[3], 'test');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('run', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', '--verbose', 'pub', 'run', '--foo', 'bar']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(4));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], 'run');
+      expect(commands[2], '--foo');
+      expect(commands[3], 'bar');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('pub publish', () async {
+      final PromptingProcess process = PromptingProcess();
+      mockProcessManager.processFactory = (List<String> commands) => process;
+      final Future<void> runPackages = createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'pub', 'publish']);
+      final Future<void> runPrompt = process.showPrompt('Proceed (y/n)? ', <String>['hello', 'world']);
+      final Future<void> simulateUserInput = Future<void>(() {
+        mockStdio.simulateStdin('y');
+      });
+      await Future.wait<void>(<Future<void>>[runPackages, runPrompt, simulateUserInput]);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(2));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], 'publish');
+      final List<String> stdout = mockStdio.writtenToStdout;
+      expect(stdout, hasLength(4));
+      expect(stdout.sublist(0, 2), contains('Proceed (y/n)? '));
+      expect(stdout.sublist(0, 2), contains('y\n'));
+      expect(stdout[2], 'hello\n');
+      expect(stdout[3], 'world\n');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('publish', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'publish']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'publish');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('deps', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'deps']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'deps');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('cache', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'cache']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'cache');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('version', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'version']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'version');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('uploader', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'uploader']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(3));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'uploader');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+
+    testUsingContext('global', () async {
+      await createTestCommandRunner(PackagesCommand()).run(<String>['packages', 'global', 'list']);
+      final List<String> commands = mockProcessManager.commands;
+      expect(commands, hasLength(4));
+      expect(commands[0], matches(r'dart-sdk[\\/]bin[\\/]pub'));
+      expect(commands[1], '--trace');
+      expect(commands[2], 'global');
+      expect(commands[3], 'list');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Stdio: () => mockStdio,
+      BotDetector: () => const AlwaysTrueBotDetector(),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/precache_test.dart b/packages/flutter_tools/test/general.shard/commands/precache_test.dart
new file mode 100644
index 0000000..0308da6
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/precache_test.dart
@@ -0,0 +1,87 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/precache.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('precache', () {
+    final MockCache cache = MockCache();
+    Set<DevelopmentArtifact> artifacts;
+
+    when(cache.isUpToDate()).thenReturn(false);
+    when(cache.updateAll(any)).thenAnswer((Invocation invocation) {
+      artifacts = invocation.positionalArguments.first;
+      return Future<void>.value(null);
+    });
+
+    testUsingContext('Adds artifact flags to requested artifacts', () async {
+      final PrecacheCommand command = PrecacheCommand();
+      applyMocksToCommand(command);
+      await createTestCommandRunner(command).run(
+        const <String>['precache', '--ios', '--android', '--web', '--macos', '--linux', '--windows', '--fuchsia']
+      );
+      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.iOS,
+        DevelopmentArtifact.android,
+        DevelopmentArtifact.web,
+        DevelopmentArtifact.macOS,
+        DevelopmentArtifact.linux,
+        DevelopmentArtifact.windows,
+        DevelopmentArtifact.fuchsia,
+      }));
+    }, overrides: <Type, Generator>{
+      Cache: () => cache,
+    });
+
+    final MockFlutterVersion flutterVersion = MockFlutterVersion();
+    when(flutterVersion.isMaster).thenReturn(false);
+
+    testUsingContext('Adds artifact flags to requested artifacts on stable', () async {
+      // Release lock between test cases.
+      Cache.releaseLockEarly();
+      final PrecacheCommand command = PrecacheCommand();
+      applyMocksToCommand(command);
+      await createTestCommandRunner(command).run(
+       const <String>['precache', '--ios', '--android', '--web', '--macos', '--linux', '--windows', '--fuchsia']
+      );
+     expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+       DevelopmentArtifact.universal,
+       DevelopmentArtifact.iOS,
+       DevelopmentArtifact.android,
+     }));
+    }, overrides: <Type, Generator>{
+      Cache: () => cache,
+      FlutterVersion: () => flutterVersion,
+    });
+
+    testUsingContext('Downloads artifacts when --force is provided', () async {
+      when(cache.isUpToDate()).thenReturn(true);
+      // Release lock between test cases.
+      Cache.releaseLockEarly();
+      final PrecacheCommand command = PrecacheCommand();
+      applyMocksToCommand(command);
+      await createTestCommandRunner(command).run(const <String>['precache', '--force']);
+      expect(artifacts, unorderedEquals(<DevelopmentArtifact>{
+       DevelopmentArtifact.universal,
+       DevelopmentArtifact.iOS,
+       DevelopmentArtifact.android,
+     }));
+    }, overrides: <Type, Generator>{
+      Cache: () => cache,
+      FlutterVersion: () => flutterVersion,
+    });
+  });
+}
+
+class MockFlutterVersion extends Mock implements FlutterVersion {}
+class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/commands/run_test.dart b/packages/flutter_tools/test/general.shard/commands/run_test.dart
new file mode 100644
index 0000000..b743a36
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/run_test.dart
@@ -0,0 +1,262 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/run.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('run', () {
+    MockApplicationPackageFactory mockApplicationPackageFactory;
+    MockDeviceManager mockDeviceManager;
+    MockFlutterVersion mockStableFlutterVersion;
+    MockFlutterVersion mockUnstableFlutterVersion;
+
+    setUpAll(() {
+      Cache.disableLocking();
+      mockApplicationPackageFactory = MockApplicationPackageFactory();
+      mockDeviceManager = MockDeviceManager();
+      mockStableFlutterVersion = MockFlutterVersion(isStable: true);
+      mockUnstableFlutterVersion = MockFlutterVersion(isStable: false);
+    });
+
+    testUsingContext('fails when target not found', () async {
+      final RunCommand command = RunCommand();
+      applyMocksToCommand(command);
+      try {
+        await createTestCommandRunner(command).run(<String>['run', '-t', 'abc123']);
+        fail('Expect exception');
+      } on ToolExit catch (e) {
+        expect(e.exitCode ?? 1, 1);
+      }
+    });
+
+
+    group('dart-flags option', () {
+      setUpAll(() {
+        when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+          return Stream<Device>.fromIterable(<Device>[
+            FakeDevice(),
+          ]);
+        });
+      });
+
+      RunCommand command;
+      List<String> args;
+      setUp(() {
+        command = TestRunCommand();
+        args = <String> [
+          'run',
+          '--dart-flags', '"--observe"',
+          '--no-hot',
+        ];
+      });
+
+      testUsingContext('is not available on stable channel', () async {
+        // Stable branch.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        // ignore: unused_catch_clause
+        } on UsageException catch(e) {
+          // Not available while on stable branch.
+        }
+      }, overrides: <Type, Generator>{
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockStableFlutterVersion,
+      });
+
+      testUsingContext('is populated in debug mode', () async {
+        // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+
+      testUsingContext('is populated in profile mode', () async {
+        args.add('--profile');
+
+        // FakeDevice.startApp checks that --dart-flags doesn't get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is populated.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+
+      testUsingContext('is not populated in release mode', () async {
+        args.add('--release');
+
+        // FakeDevice.startApp checks that --dart-flags *does* get dropped and
+        // throws ToolExit with FakeDevice.kSuccess if the flag is set to the
+        // empty string.
+        try {
+          await createTestCommandRunner(command).run(args);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.exitCode, FakeDevice.kSuccess);
+        }
+      }, overrides: <Type, Generator>{
+        ApplicationPackageFactory: () => mockApplicationPackageFactory,
+        DeviceManager: () => mockDeviceManager,
+        FlutterVersion: () => mockUnstableFlutterVersion,
+      });
+    });
+
+    testUsingContext('should only request artifacts corresponding to connected devices', () async {
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.android_arm),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.android,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.ios),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.iOS,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.ios),
+          MockDevice(TargetPlatform.android_arm),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.iOS,
+        DevelopmentArtifact.android,
+      }));
+
+      when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+        return Stream<Device>.fromIterable(<Device>[
+          MockDevice(TargetPlatform.web_javascript),
+        ]);
+      });
+
+      expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
+        DevelopmentArtifact.universal,
+        DevelopmentArtifact.web,
+      }));
+    }, overrides: <Type, Generator>{
+      DeviceManager: () => mockDeviceManager,
+    });
+  });
+}
+
+class MockDeviceManager extends Mock implements DeviceManager {}
+class MockDevice extends Mock implements Device {
+  MockDevice(this._targetPlatform);
+
+  final TargetPlatform _targetPlatform;
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => _targetPlatform;
+}
+
+class TestRunCommand extends RunCommand {
+  @override
+  // ignore: must_call_super
+  Future<void> validateCommand() async {
+    devices = await deviceManager.getDevices().toList();
+  }
+}
+
+class MockStableFlutterVersion extends MockFlutterVersion {
+  @override
+  bool get isMaster => false;
+}
+
+class FakeDevice extends Fake implements Device {
+  static const int kSuccess = 1;
+  static const int kFailure = -1;
+  final TargetPlatform _targetPlatform = TargetPlatform.ios;
+
+  void _throwToolExit(int code) => throwToolExit(null, exitCode: code);
+
+  @override
+  Future<bool> get isLocalEmulator => Future<bool>.value(false);
+
+  @override
+  bool get supportsHotReload => false;
+
+  @override
+  Future<String> get sdkNameAndVersion => Future<String>.value('');
+
+  @override
+  DeviceLogReader getLogReader({ ApplicationPackage app }) {
+    return MockDeviceLogReader();
+  }
+
+  @override
+  String get name => 'FakeDevice';
+
+  @override
+  Future<TargetPlatform> get targetPlatform async => _targetPlatform;
+
+  @override
+  Future<LaunchResult> startApp(
+    ApplicationPackage package, {
+    String mainPath,
+    String route,
+    DebuggingOptions debuggingOptions,
+    Map<String, dynamic> platformArgs,
+    bool prebuiltApplication = false,
+    bool usesTerminalUi = true,
+    bool ipv6 = false,
+  }) async {
+    final String dartFlags = debuggingOptions.dartFlags;
+    // In release mode, --dart-flags should be set to the empty string and
+    // provided flags should be dropped. In debug and profile modes,
+    // --dart-flags should not be empty.
+    if (debuggingOptions.buildInfo.isRelease) {
+      if (dartFlags.isNotEmpty) {
+        _throwToolExit(kFailure);
+      }
+      _throwToolExit(kSuccess);
+    } else {
+      if (dartFlags.isEmpty) {
+        _throwToolExit(kFailure);
+      }
+      _throwToolExit(kSuccess);
+    }
+    return null;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart b/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart
new file mode 100644
index 0000000..00b4aac
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/shell_completion_test.dart
@@ -0,0 +1,91 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.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/cache.dart';
+import 'package:flutter_tools/src/commands/shell_completion.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('shell_completion', () {
+    MockStdio mockStdio;
+
+    setUp(() {
+      Cache.disableLocking();
+      mockStdio = MockStdio();
+    });
+
+    testUsingContext('generates bash initialization script to stdout', () async {
+      final ShellCompletionCommand command = ShellCompletionCommand();
+      await createTestCommandRunner(command).run(<String>['bash-completion']);
+      expect(mockStdio.writtenToStdout.length, equals(1));
+      expect(mockStdio.writtenToStdout.first, contains('__flutter_completion'));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('generates bash initialization script to stdout with arg', () async {
+      final ShellCompletionCommand command = ShellCompletionCommand();
+      await createTestCommandRunner(command).run(<String>['bash-completion', '-']);
+      expect(mockStdio.writtenToStdout.length, equals(1));
+      expect(mockStdio.writtenToStdout.first, contains('__flutter_completion'));
+    }, overrides: <Type, Generator>{
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('generates bash initialization script to output file', () async {
+      final ShellCompletionCommand command = ShellCompletionCommand();
+      const String outputFile = 'bash-setup.sh';
+      await createTestCommandRunner(command).run(
+        <String>['bash-completion', outputFile],
+      );
+      expect(fs.isFileSync(outputFile), isTrue);
+      expect(fs.file(outputFile).readAsStringSync(), contains('__flutter_completion'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext("won't overwrite existing output file ", () async {
+      final ShellCompletionCommand command = ShellCompletionCommand();
+      const String outputFile = 'bash-setup.sh';
+      fs.file(outputFile).createSync();
+      try {
+        await createTestCommandRunner(command).run(
+          <String>['bash-completion', outputFile],
+        );
+        fail('Expect ToolExit exception');
+      } on ToolExit catch (error) {
+        expect(error.exitCode ?? 1, 1);
+        expect(error.message, contains('Use --overwrite'));
+      }
+      expect(fs.isFileSync(outputFile), isTrue);
+      expect(fs.file(outputFile).readAsStringSync(), isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Stdio: () => mockStdio,
+    });
+
+    testUsingContext('will overwrite existing output file if given --overwrite', () async {
+      final ShellCompletionCommand command = ShellCompletionCommand();
+      const String outputFile = 'bash-setup.sh';
+      fs.file(outputFile).createSync();
+      await createTestCommandRunner(command).run(
+        <String>['bash-completion', '--overwrite', outputFile],
+      );
+      expect(fs.isFileSync(outputFile), isTrue);
+      expect(fs.file(outputFile).readAsStringSync(), contains('__flutter_completion'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Stdio: () => mockStdio,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/test_test.dart b/packages/flutter_tools/test/general.shard/commands/test_test.dart
new file mode 100644
index 0000000..348ce99
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/test_test.dart
@@ -0,0 +1,221 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/dart/sdk.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+// This test depends on some files in ///dev/automated_tests/flutter_test/*
+
+Future<void> _testExclusionLock;
+
+void main() {
+  group('flutter test should', () {
+
+    final String automatedTestsDirectory = fs.path.join('..', '..', 'dev', 'automated_tests');
+    final String flutterTestDirectory = fs.path.join(automatedTestsDirectory, 'flutter_test');
+
+    testUsingContext('not have extraneous error messages', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('trivial_widget', automatedTestsDirectory, flutterTestDirectory, exitCode: isZero);
+    }, skip: io.Platform.isLinux); // Flutter on Linux sometimes has problems with font resolution (#7224)
+
+    testUsingContext('report nice errors for exceptions thrown within testWidgets()', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report a nice error when a guarded function was called without await', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report a nice error when an async function was called without await', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report a nice error when a Ticker is left running', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('ticker', automatedTestsDirectory, flutterTestDirectory);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async {
+      final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
+      Cache.flutterRoot = '../..';
+      return _testFile('trivial', missingDependencyTests, missingDependencyTests);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report which user created widget caused the error', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('print_user_created_ancestor', automatedTestsDirectory, flutterTestDirectory,
+          extraArguments: const <String>['--track-widget-creation']);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('report which user created widget caused the error - no flag', () async {
+      Cache.flutterRoot = '../..';
+      return _testFile('print_user_created_ancestor_no_flag', automatedTestsDirectory, flutterTestDirectory);
+    }, skip: io.Platform.isWindows); // TODO(chunhtai): Dart on Windows has trouble with unicode characters in output (#35425).
+
+    testUsingContext('run a test when its name matches a regexp', () async {
+      Cache.flutterRoot = '../..';
+      final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
+        extraArguments: const <String>['--name', 'inc.*de']);
+      if (!result.stdout.contains('+1: All tests passed'))
+        fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
+      expect(result.exitCode, 0);
+    });
+
+    testUsingContext('run a test when its name contains a string', () async {
+      Cache.flutterRoot = '../..';
+      final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
+        extraArguments: const <String>['--plain-name', 'include']);
+      if (!result.stdout.contains('+1: All tests passed'))
+        fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
+      expect(result.exitCode, 0);
+    });
+
+    testUsingContext('test runs to completion', () async {
+      Cache.flutterRoot = '../..';
+      final ProcessResult result = await _runFlutterTest('trivial', automatedTestsDirectory, flutterTestDirectory,
+        extraArguments: const <String>['--verbose']);
+      if ((!result.stdout.contains('+1: All tests passed')) ||
+          (!result.stdout.contains('test 0: starting shell process')) ||
+          (!result.stdout.contains('test 0: deleting temporary directory')) ||
+          (!result.stdout.contains('test 0: finished')) ||
+          (!result.stdout.contains('test package returned with exit code 0')))
+        fail('unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n');
+      if (result.stderr.isNotEmpty)
+        fail('unexpected error output from test:\n\n${result.stderr}\n-- end stderr --\n\n');
+      expect(result.exitCode, 0);
+    });
+
+  });
+}
+
+Future<void> _testFile(
+    String testName,
+    String workingDirectory,
+    String testDirectory, {
+      Matcher exitCode,
+      List<String> extraArguments = const <String>[],
+    }) async {
+  exitCode ??= isNonZero;
+  final String fullTestExpectation = fs.path.join(testDirectory, '${testName}_expectation.txt');
+  final File expectationFile = fs.file(fullTestExpectation);
+  if (!expectationFile.existsSync())
+    fail('missing expectation file: $expectationFile');
+
+  while (_testExclusionLock != null)
+    await _testExclusionLock;
+
+  final ProcessResult exec = await _runFlutterTest(
+    testName,
+    workingDirectory,
+    testDirectory,
+    extraArguments: extraArguments,
+  );
+
+  expect(exec.exitCode, exitCode);
+  final List<String> output = exec.stdout.split('\n');
+  if (output.first == 'Waiting for another flutter command to release the startup lock...')
+    output.removeAt(0);
+  if (output.first.startsWith('Running "flutter pub get" in'))
+    output.removeAt(0);
+  output.add('<<stderr>>');
+  output.addAll(exec.stderr.split('\n'));
+  final List<String> expectations = fs.file(fullTestExpectation).readAsLinesSync();
+  bool allowSkip = false;
+  int expectationLineNumber = 0;
+  int outputLineNumber = 0;
+  bool haveSeenStdErrMarker = false;
+  while (expectationLineNumber < expectations.length) {
+    expect(
+      output,
+      hasLength(greaterThan(outputLineNumber)),
+      reason: 'Failure in $testName to compare to $fullTestExpectation',
+    );
+    final String expectationLine = expectations[expectationLineNumber];
+    String outputLine = output[outputLineNumber];
+    if (expectationLine == '<<skip until matching line>>') {
+      allowSkip = true;
+      expectationLineNumber += 1;
+      continue;
+    }
+    if (allowSkip) {
+      if (!RegExp(expectationLine).hasMatch(outputLine)) {
+        outputLineNumber += 1;
+        continue;
+      }
+      allowSkip = false;
+    }
+    if (expectationLine == '<<stderr>>') {
+      expect(haveSeenStdErrMarker, isFalse);
+      haveSeenStdErrMarker = true;
+    }
+    if (!RegExp(expectationLine).hasMatch(outputLine) && outputLineNumber + 1 < output.length) {
+      // Check if the RegExp can match the next two lines in the output so
+      // that it is possible to write expectations that still hold even if a
+      // line is wrapped slightly differently due to for example a file name
+      // being longer on one platform than another.
+      final String mergedLines = '$outputLine\n${output[outputLineNumber+1]}';
+      if (RegExp(expectationLine).hasMatch(mergedLines)) {
+        outputLineNumber += 1;
+        outputLine = mergedLines;
+      }
+    }
+
+    expect(outputLine, matches(expectationLine), reason: 'Full output:\n- - - -----8<----- - - -\n${output.join("\n")}\n- - - -----8<----- - - -');
+    expectationLineNumber += 1;
+    outputLineNumber += 1;
+  }
+  expect(allowSkip, isFalse);
+  if (!haveSeenStdErrMarker)
+    expect(exec.stderr, '');
+}
+
+Future<ProcessResult> _runFlutterTest(
+  String testName,
+  String workingDirectory,
+  String testDirectory, {
+  List<String> extraArguments = const <String>[],
+}) async {
+
+  final String testFilePath = fs.path.join(testDirectory, '${testName}_test.dart');
+  final File testFile = fs.file(testFilePath);
+  if (!testFile.existsSync())
+    fail('missing test file: $testFile');
+
+  final List<String> args = <String>[
+    ...dartVmFlags,
+    fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')),
+    'test',
+    '--no-color',
+    ...extraArguments,
+    testFilePath
+  ];
+
+  while (_testExclusionLock != null)
+    await _testExclusionLock;
+
+  final Completer<void> testExclusionCompleter = Completer<void>();
+  _testExclusionLock = testExclusionCompleter.future;
+  try {
+    return await Process.run(
+      fs.path.join(dartSdkPath, 'bin', 'dart'),
+      args,
+      workingDirectory: workingDirectory,
+    );
+  } finally {
+    _testExclusionLock = null;
+    testExclusionCompleter.complete();
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart b/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart
new file mode 100644
index 0000000..6097ba0
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/update_packages_test.dart
@@ -0,0 +1,18 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/commands/update_packages.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('UpdatePackagesCommand', () {
+    // Marking it as experimental breaks bots tests and packaging scripts on stable branches.
+    testUsingContext('is not marked as experimental', () async {
+      final UpdatePackagesCommand command = UpdatePackagesCommand();
+      expect(command.isExperimental, isFalse);
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart b/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart
new file mode 100644
index 0000000..ac9879d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/upgrade_test.dart
@@ -0,0 +1,192 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/upgrade.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('UpgradeCommandRunner', () {
+    FakeUpgradeCommandRunner fakeCommandRunner;
+    UpgradeCommandRunner realCommandRunner;
+    MockProcessManager processManager;
+    final MockFlutterVersion flutterVersion = MockFlutterVersion();
+    const GitTagVersion gitTagVersion = GitTagVersion(1, 2, 3, 4, 5, 'asd');
+    when(flutterVersion.channel).thenReturn('dev');
+
+    setUp(() {
+      fakeCommandRunner = FakeUpgradeCommandRunner();
+      realCommandRunner = UpgradeCommandRunner();
+      processManager = MockProcessManager();
+      fakeCommandRunner.willHaveUncomittedChanges = false;
+    });
+
+    test('throws on unknown tag, official branch,  noforce', () async {
+      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
+        false,
+        const GitTagVersion.unknown(),
+        flutterVersion,
+      );
+      expect(result, throwsA(isInstanceOf<ToolExit>()));
+    });
+
+    test('does not throw on unknown tag, official branch, force', () async {
+      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
+        true,
+        const GitTagVersion.unknown(),
+        flutterVersion,
+      );
+      expect(await result, null);
+    });
+
+    test('throws tool exit with uncommitted changes', () async {
+      fakeCommandRunner.willHaveUncomittedChanges = true;
+      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
+        false,
+        gitTagVersion,
+        flutterVersion,
+      );
+      expect(result, throwsA(isA<ToolExit>()));
+    });
+
+    test('does not throw tool exit with uncommitted changes and force', () async {
+      fakeCommandRunner.willHaveUncomittedChanges = true;
+      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
+        true,
+        gitTagVersion,
+        flutterVersion,
+      );
+      expect(await result, null);
+    });
+
+    test('Doesn\'t throw on known tag, dev branch, no force', () async {
+      final Future<FlutterCommandResult> result = fakeCommandRunner.runCommand(
+        false,
+        gitTagVersion,
+        flutterVersion,
+      );
+      expect(await result, null);
+    });
+
+    testUsingContext('verifyUpstreamConfigured', () async {
+      when(processManager.run(
+        <String>['git', 'rev-parse', '@{u}'],
+        environment:anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'))
+      ).thenAnswer((Invocation invocation) async {
+        return FakeProcessResult()
+          ..exitCode = 0;
+      });
+      await realCommandRunner.verifyUpstreamConfigured();
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+  });
+
+  group('matchesGitLine', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    bool _match(String line) => UpgradeCommandRunner.matchesGitLine(line);
+
+    test('regex match', () {
+      expect(_match(' .../flutter_gallery/lib/demo/buttons_demo.dart    | 10 +--'), true);
+      expect(_match(' dev/benchmarks/complex_layout/lib/main.dart        |  24 +-'), true);
+
+      expect(_match(' rename {packages/flutter/doc => dev/docs}/styles.html (92%)'), true);
+      expect(_match(' delete mode 100644 doc/index.html'), true);
+      expect(_match(' create mode 100644 examples/flutter_gallery/lib/gallery/demo.dart'), true);
+
+      expect(_match('Fast-forward'), true);
+    });
+
+    test('regex doesn\'t match', () {
+      expect(_match('Updating 79cfe1e..5046107'), false);
+      expect(_match('229 files changed, 6179 insertions(+), 3065 deletions(-)'), false);
+    });
+
+    group('findProjectRoot', () {
+      Directory tempDir;
+
+      setUp(() async {
+        tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_upgrade_test.');
+      });
+
+      tearDown(() {
+        tryToDelete(tempDir);
+      });
+
+      testUsingContext('in project', () async {
+        final String projectPath = await createProject(tempDir);
+        expect(findProjectRoot(projectPath), projectPath);
+        expect(findProjectRoot(fs.path.join(projectPath, 'lib')), projectPath);
+
+        final String hello = fs.path.join(Cache.flutterRoot, 'examples', 'hello_world');
+        expect(findProjectRoot(hello), hello);
+        expect(findProjectRoot(fs.path.join(hello, 'lib')), hello);
+      });
+
+      testUsingContext('outside project', () async {
+        final String projectPath = await createProject(tempDir);
+        expect(findProjectRoot(fs.directory(projectPath).parent.path), null);
+        expect(findProjectRoot(Cache.flutterRoot), null);
+      });
+    });
+  });
+}
+
+class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
+  bool willHaveUncomittedChanges = false;
+
+  @override
+  Future<void> verifyUpstreamConfigured() async {}
+
+  @override
+  Future<bool> hasUncomittedChanges() async => willHaveUncomittedChanges;
+
+  @override
+  Future<void> resetChanges(GitTagVersion gitTagVersion) async {}
+
+  @override
+  Future<void> upgradeChannel(FlutterVersion flutterVersion) async {}
+
+  @override
+  Future<void> attemptFastForward() async {}
+
+  @override
+  Future<void> precacheArtifacts() async {}
+
+  @override
+  Future<void> updatePackages(FlutterVersion flutterVersion) async {}
+
+  @override
+  Future<void> runDoctor() async {}
+}
+
+class MockFlutterVersion extends Mock implements FlutterVersion {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class FakeProcessResult implements ProcessResult {
+  @override
+  int exitCode;
+
+  @override
+  int pid = 0;
+
+  @override
+  String stderr = '';
+
+  @override
+  String stdout = '';
+}
diff --git a/packages/flutter_tools/test/general.shard/commands/version_test.dart b/packages/flutter_tools/test/general.shard/commands/version_test.dart
new file mode 100644
index 0000000..1e48004
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/version_test.dart
@@ -0,0 +1,123 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart' show MockProcess;
+
+void main() {
+  group('version', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    testUsingContext('version ls', () async {
+      final VersionCommand command = VersionCommand();
+      await createTestCommandRunner(command).run(<String>['version']);
+      expect(testLogger.statusText, equals('v10.0.0\r\nv20.0.0\n' ''));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('version switch', () async {
+      const String version = '10.0.0';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.statusText, contains('Switching Flutter to version $version'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('switch to not supported version without force', () async {
+      const String version = '1.1.5';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.errorText, contains('Version command is not supported in'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+
+    testUsingContext('switch to not supported version with force', () async {
+      const String version = '1.1.5';
+      final VersionCommand command = VersionCommand();
+      final Future<void> runCommand = createTestCommandRunner(command).run(<String>['version', '--force', version]);
+      await Future.wait<void>(<Future<void>>[runCommand]);
+      expect(testLogger.statusText, contains('Switching Flutter to version $version with force'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => MockProcessManager(),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {
+  String version = '';
+
+  @override
+  Future<ProcessResult> run(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) async {
+    if (command[0] == 'git' && command[1] == 'tag') {
+      return ProcessResult(0, 0, 'v10.0.0\r\nv20.0.0', '');
+    }
+    if (command[0] == 'git' && command[1] == 'checkout') {
+      version = command[2];
+    }
+    return ProcessResult(0, 0, '', '');
+  }
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    final String commandStr = command.join(' ');
+    if (commandStr == 'git log -n 1 --pretty=format:%H') {
+      return ProcessResult(0, 0, '000000000000000000000', '');
+    }
+    if (commandStr ==
+        'git describe --match v*.*.* --first-parent --long --tags') {
+      if (version.isNotEmpty) {
+        return ProcessResult(0, 0, '$version-0-g00000000', '');
+      }
+    }
+    return ProcessResult(0, 0, '', '');
+  }
+
+  @override
+  Future<Process> start(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    ProcessStartMode mode = ProcessStartMode.normal,
+  }) {
+    final Completer<Process> completer = Completer<Process>();
+    completer.complete(MockProcess());
+    return completer.future;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/compile_test.dart b/packages/flutter_tools/test/general.shard/compile_test.dart
new file mode 100644
index 0000000..cb70e7f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/compile_test.dart
@@ -0,0 +1,593 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/compile.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+
+void main() {
+  group(PackageUriMapper, () {
+    group('single-root', () {
+      const String packagesContents = r'''
+xml:file:///Users/flutter_user/.pub-cache/hosted/pub.dartlang.org/xml-3.2.3/lib/
+yaml:file:///Users/flutter_user/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.15/lib/
+example:file:///example/lib/
+''';
+      final MockFileSystem mockFileSystem = MockFileSystem();
+      final MockFile mockFile = MockFile();
+      when(mockFileSystem.path).thenReturn(fs.path);
+      when(mockFileSystem.file(any)).thenReturn(mockFile);
+      when(mockFile.readAsBytesSync()).thenReturn(utf8.encode(packagesContents));
+      testUsingContext('Can map main.dart to correct package', () async {
+        final PackageUriMapper packageUriMapper = PackageUriMapper('/example/lib/main.dart', '.packages', null, null);
+        expect(packageUriMapper.map('/example/lib/main.dart').toString(), 'package:example/main.dart');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => mockFileSystem,
+      });
+
+      testUsingContext('Maps file from other package to null', () async {
+        final PackageUriMapper packageUriMapper = PackageUriMapper('/example/lib/main.dart', '.packages', null, null);
+        expect(packageUriMapper.map('/xml/lib/xml.dart'),  null);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => mockFileSystem,
+      });
+
+      testUsingContext('Maps non-main file from same package', () async {
+        final PackageUriMapper packageUriMapper = PackageUriMapper('/example/lib/main.dart', '.packages', null, null);
+        expect(packageUriMapper.map('/example/lib/src/foo.dart').toString(), 'package:example/src/foo.dart');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => mockFileSystem,
+      });
+    });
+
+    group('multi-root', () {
+      final MockFileSystem mockFileSystem = MockFileSystem();
+      final MockFile mockFile = MockFile();
+      when(mockFileSystem.path).thenReturn(fs.path);
+      when(mockFileSystem.file(any)).thenReturn(mockFile);
+
+      const String multiRootPackagesContents = r'''
+xml:file:///Users/flutter_user/.pub-cache/hosted/pub.dartlang.org/xml-3.2.3/lib/
+yaml:file:///Users/flutter_user/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.15/lib/
+example:org-dartlang-app:/
+''';
+      when(mockFile.readAsBytesSync()).thenReturn(utf8.encode(multiRootPackagesContents));
+
+      testUsingContext('Maps main file from same package on multiroot scheme', () async {
+        final PackageUriMapper packageUriMapper = PackageUriMapper('/example/lib/main.dart', '.packages', 'org-dartlang-app', <String>['/example/lib/', '/gen/lib/']);
+        expect(packageUriMapper.map('/example/lib/main.dart').toString(), 'package:example/main.dart');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => mockFileSystem,
+      });
+    });
+  });
+
+  testUsingContext('StdOutHandler test', () async {
+    final StdoutHandler stdoutHandler = StdoutHandler();
+    stdoutHandler.handler('result 12345');
+    expect(stdoutHandler.boundaryKey, '12345');
+    stdoutHandler.handler('12345');
+    stdoutHandler.handler('12345 message 0');
+    final CompilerOutput output = await stdoutHandler.compilerOutput.future;
+    expect(output.errorCount, 0);
+    expect(output.outputFilename, 'message');
+  }, overrides: <Type, Generator>{
+    Logger: () => BufferLogger(),
+  });
+
+  group('batch compile', () {
+    ProcessManager mockProcessManager;
+    MockProcess mockFrontendServer;
+    MockStdIn mockFrontendServerStdIn;
+    MockStream mockFrontendServerStdErr;
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockFrontendServer = MockProcess();
+      mockFrontendServerStdIn = MockStdIn();
+      mockFrontendServerStdErr = MockStream();
+
+      when(mockFrontendServer.stderr)
+          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
+      final StreamController<String> stdErrStreamController = StreamController<String>();
+      when(mockFrontendServerStdErr.transform<String>(any)).thenAnswer((_) => stdErrStreamController.stream);
+      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+      when(mockProcessManager.start(any)).thenAnswer(
+          (Invocation invocation) => Future<Process>.value(mockFrontendServer));
+      when(mockFrontendServer.exitCode).thenAnswer((_) async => 0);
+    });
+
+    testUsingContext('single dart successful compilation', () async {
+      final BufferLogger logger = context.get<Logger>();
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0'
+            ))
+          ));
+      final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(null);
+      final CompilerOutput output = await kernelCompiler.compile(sdkRoot: '/path/to/sdkroot',
+        mainPath: '/path/to/main.dart',
+        trackWidgetCreation: false,
+      );
+      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+      expect(output.outputFilename, equals('/path/to/main.dart.dill'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('single dart failed compilation', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'result abc\nline1\nline2\nabc\nabc'
+            ))
+          ));
+      final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(null);
+      final CompilerOutput output = await kernelCompiler.compile(sdkRoot: '/path/to/sdkroot',
+        mainPath: '/path/to/main.dart',
+        trackWidgetCreation: false,
+      );
+      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+      expect(output, equals(null));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('single dart abnormal compiler termination', () async {
+      when(mockFrontendServer.exitCode).thenAnswer((_) async => 255);
+
+      final BufferLogger logger = context.get<Logger>();
+
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+          Future<List<int>>.value(utf8.encode(
+              'result abc\nline1\nline2\nabc\nabc'
+          ))
+      ));
+      final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(null);
+      final CompilerOutput output = await kernelCompiler.compile(
+        sdkRoot: '/path/to/sdkroot',
+        mainPath: '/path/to/main.dart',
+        trackWidgetCreation: false,
+      );
+      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+      expect(output, equals(null));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+  });
+
+  group('incremental compile', () {
+    ProcessManager mockProcessManager;
+    ResidentCompiler generator;
+    MockProcess mockFrontendServer;
+    MockStdIn mockFrontendServerStdIn;
+    MockStream mockFrontendServerStdErr;
+    StreamController<String> stdErrStreamController;
+
+    setUp(() {
+      generator = ResidentCompiler('sdkroot');
+      mockProcessManager = MockProcessManager();
+      mockFrontendServer = MockProcess();
+      mockFrontendServerStdIn = MockStdIn();
+      mockFrontendServerStdErr = MockStream();
+
+      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
+      when(mockFrontendServer.stderr)
+          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
+      stdErrStreamController = StreamController<String>();
+      when(mockFrontendServerStdErr.transform<String>(any))
+          .thenAnswer((Invocation invocation) => stdErrStreamController.stream);
+
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+      when(mockProcessManager.start(any)).thenAnswer(
+          (Invocation invocation) => Future<Process>.value(mockFrontendServer)
+      );
+    });
+
+    tearDown(() {
+      verifyNever(mockFrontendServer.exitCode);
+    });
+
+    testUsingContext('single dart compile', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0'
+            ))
+          ));
+
+      final CompilerOutput output = await generator.recompile(
+        '/path/to/main.dart',
+          null /* invalidatedFiles */,
+        outputPath: '/build/',
+      );
+      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
+      verifyNoMoreInteractions(mockFrontendServerStdIn);
+      expect(logger.errorText, equals('\nCompiler message:\nline1\nline2\n'));
+      expect(output.outputFilename, equals('/path/to/main.dart.dill'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('single dart compile abnormally terminates', () async {
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => const Stream<List<int>>.empty()
+      );
+
+      final CompilerOutput output = await generator.recompile(
+        '/path/to/main.dart',
+        null, /* invalidatedFiles */
+        outputPath: '/build/',
+      );
+      expect(output, equals(null));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('compile and recompile', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      final StreamController<List<int>> streamController = StreamController<List<int>>();
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => streamController.stream);
+      streamController.add(utf8.encode('result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n'));
+      await generator.recompile(
+        '/path/to/main.dart',
+        null, /* invalidatedFiles */
+        outputPath: '/build/',
+      );
+      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
+
+      // No accept or reject commands should be issued until we
+      // send recompile request.
+      await _accept(streamController, generator, mockFrontendServerStdIn, '');
+      await _reject(streamController, generator, mockFrontendServerStdIn, '', '');
+
+      await _recompile(streamController, generator, mockFrontendServerStdIn,
+        'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
+
+      await _accept(streamController, generator, mockFrontendServerStdIn, '^accept\\n\$');
+
+      await _recompile(streamController, generator, mockFrontendServerStdIn,
+          'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
+
+      await _reject(streamController, generator, mockFrontendServerStdIn, 'result abc\nabc\nabc\nabc',
+          '^reject\\n\$');
+
+      verifyNoMoreInteractions(mockFrontendServerStdIn);
+      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+      expect(logger.errorText, equals(
+        '\nCompiler message:\nline0\nline1\n'
+        '\nCompiler message:\nline1\nline2\n'
+        '\nCompiler message:\nline1\nline2\n'
+      ));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('compile and recompile twice', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      final StreamController<List<int>> streamController = StreamController<List<int>>();
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) => streamController.stream);
+      streamController.add(utf8.encode(
+        'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0\n'
+      ));
+      await generator.recompile('/path/to/main.dart', null /* invalidatedFiles */, outputPath: '/build/');
+      expect(mockFrontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
+
+      await _recompile(streamController, generator, mockFrontendServerStdIn,
+        'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
+      await _recompile(streamController, generator, mockFrontendServerStdIn,
+        'result abc\nline2\nline3\nabc\nabc /path/to/main.dart.dill 0\n');
+
+      verifyNoMoreInteractions(mockFrontendServerStdIn);
+      expect(mockFrontendServerStdIn.getAndClear(), isEmpty);
+      expect(logger.errorText, equals(
+        '\nCompiler message:\nline0\nline1\n'
+        '\nCompiler message:\nline1\nline2\n'
+        '\nCompiler message:\nline2\nline3\n'
+      ));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+  });
+
+  group('compile expression', () {
+    ProcessManager mockProcessManager;
+    ResidentCompiler generator;
+    MockProcess mockFrontendServer;
+    MockStdIn mockFrontendServerStdIn;
+    MockStream mockFrontendServerStdErr;
+    StreamController<String> stdErrStreamController;
+
+    setUp(() {
+      generator = ResidentCompiler('sdkroot');
+      mockProcessManager = MockProcessManager();
+      mockFrontendServer = MockProcess();
+      mockFrontendServerStdIn = MockStdIn();
+      mockFrontendServerStdErr = MockStream();
+
+      when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
+      when(mockFrontendServer.stderr)
+          .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
+      stdErrStreamController = StreamController<String>();
+      when(mockFrontendServerStdErr.transform<String>(any))
+          .thenAnswer((Invocation invocation) => stdErrStreamController.stream);
+
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+      when(mockProcessManager.start(any)).thenAnswer(
+              (Invocation invocation) =>
+          Future<Process>.value(mockFrontendServer)
+      );
+    });
+
+    tearDown(() {
+      verifyNever(mockFrontendServer.exitCode);
+    });
+
+    testUsingContext('fails if not previously compiled', () async {
+      final CompilerOutput result = await generator.compileExpression(
+          '2+2', null, null, null, null, false);
+      expect(result, isNull);
+    });
+
+    testUsingContext('compile single expression', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      final Completer<List<int>> compileResponseCompleter =
+          Completer<List<int>>();
+      final Completer<List<int>> compileExpressionResponseCompleter =
+          Completer<List<int>>();
+
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) =>
+      Stream<List<int>>.fromFutures(
+        <Future<List<int>>>[
+          compileResponseCompleter.future,
+          compileExpressionResponseCompleter.future]));
+
+      compileResponseCompleter.complete(Future<List<int>>.value(utf8.encode(
+        'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'
+      )));
+
+      await generator.recompile(
+        '/path/to/main.dart',
+        null, /* invalidatedFiles */
+        outputPath: '/build/',
+      ).then((CompilerOutput output) {
+        expect(mockFrontendServerStdIn.getAndClear(),
+            'compile /path/to/main.dart\n');
+        verifyNoMoreInteractions(mockFrontendServerStdIn);
+        expect(logger.errorText,
+            equals('\nCompiler message:\nline1\nline2\n'));
+        expect(output.outputFilename, equals('/path/to/main.dart.dill'));
+
+        compileExpressionResponseCompleter.complete(
+            Future<List<int>>.value(utf8.encode(
+                'result def\nline1\nline2\ndef\ndef /path/to/main.dart.dill.incremental 0\n'
+            )));
+        generator.compileExpression(
+            '2+2', null, null, null, null, false).then(
+                (CompilerOutput outputExpression) {
+                  expect(outputExpression, isNotNull);
+                  expect(outputExpression.outputFilename, equals('/path/to/main.dart.dill.incremental'));
+                  expect(outputExpression.errorCount, 0);
+                }
+        );
+      });
+
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+
+    testUsingContext('compile expressions without awaiting', () async {
+      final BufferLogger logger = context.get<Logger>();
+
+      final Completer<List<int>> compileResponseCompleter = Completer<List<int>>();
+      final Completer<List<int>> compileExpressionResponseCompleter1 = Completer<List<int>>();
+      final Completer<List<int>> compileExpressionResponseCompleter2 = Completer<List<int>>();
+
+      when(mockFrontendServer.stdout)
+          .thenAnswer((Invocation invocation) =>
+      Stream<List<int>>.fromFutures(
+          <Future<List<int>>>[
+            compileResponseCompleter.future,
+            compileExpressionResponseCompleter1.future,
+            compileExpressionResponseCompleter2.future,
+          ]));
+
+      // The test manages timing via completers.
+      unawaited(
+        generator.recompile(
+          '/path/to/main.dart',
+          null, /* invalidatedFiles */
+          outputPath: '/build/',
+        ).then((CompilerOutput outputCompile) {
+          expect(logger.errorText,
+              equals('\nCompiler message:\nline1\nline2\n'));
+          expect(outputCompile.outputFilename, equals('/path/to/main.dart.dill'));
+
+          compileExpressionResponseCompleter1.complete(Future<List<int>>.value(utf8.encode(
+              'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
+          )));
+        }),
+      );
+
+      // The test manages timing via completers.
+      final Completer<bool> lastExpressionCompleted = Completer<bool>();
+      unawaited(
+        generator.compileExpression('0+1', null, null, null, null, false).then(
+          (CompilerOutput outputExpression) {
+            expect(outputExpression, isNotNull);
+            expect(outputExpression.outputFilename,
+                equals('/path/to/main.dart.dill.incremental'));
+            expect(outputExpression.errorCount, 0);
+            compileExpressionResponseCompleter2.complete(Future<List<int>>.value(utf8.encode(
+                'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
+            )));
+          },
+        ),
+      );
+
+      // The test manages timing via completers.
+      unawaited(
+        generator.compileExpression('1+1', null, null, null, null, false).then(
+          (CompilerOutput outputExpression) {
+            expect(outputExpression, isNotNull);
+            expect(outputExpression.outputFilename,
+                equals('/path/to/main.dart.dill.incremental'));
+            expect(outputExpression.errorCount, 0);
+            lastExpressionCompleted.complete(true);
+          },
+        )
+      );
+
+      compileResponseCompleter.complete(Future<List<int>>.value(utf8.encode(
+          'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n'
+      )));
+
+      expect(await lastExpressionCompleted.future, isTrue);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(showColor: false),
+      Logger: () => BufferLogger(),
+      Platform: _kNoColorTerminalPlatform,
+    });
+  });
+}
+
+Future<void> _recompile(
+  StreamController<List<int>> streamController,
+  ResidentCompiler generator,
+  MockStdIn mockFrontendServerStdIn,
+  String mockCompilerOutput,
+) async {
+  // Put content into the output stream after generator.recompile gets
+  // going few lines below, resets completer.
+  scheduleMicrotask(() {
+    streamController.add(utf8.encode(mockCompilerOutput));
+  });
+  final CompilerOutput output = await generator.recompile(
+    null /* mainPath */,
+    <Uri>[Uri.parse('/path/to/main.dart')],
+    outputPath: '/build/',
+  );
+  expect(output.outputFilename, equals('/path/to/main.dart.dill'));
+  final String commands = mockFrontendServerStdIn.getAndClear();
+  final RegExp re = RegExp('^recompile (.*)\\n/path/to/main.dart\\n(.*)\\n\$');
+  expect(commands, matches(re));
+  final Match match = re.firstMatch(commands);
+  expect(match[1] == match[2], isTrue);
+  mockFrontendServerStdIn._stdInWrites.clear();
+}
+
+Future<void> _accept(
+  StreamController<List<int>> streamController,
+  ResidentCompiler generator,
+  MockStdIn mockFrontendServerStdIn,
+  String expected,
+) async {
+  // Put content into the output stream after generator.recompile gets
+  // going few lines below, resets completer.
+  generator.accept();
+  final String commands = mockFrontendServerStdIn.getAndClear();
+  final RegExp re = RegExp(expected);
+  expect(commands, matches(re));
+  mockFrontendServerStdIn._stdInWrites.clear();
+}
+
+Future<void> _reject(
+  StreamController<List<int>> streamController,
+  ResidentCompiler generator,
+  MockStdIn mockFrontendServerStdIn,
+  String mockCompilerOutput,
+  String expected,
+) async {
+  // Put content into the output stream after generator.recompile gets
+  // going few lines below, resets completer.
+  scheduleMicrotask(() {
+    streamController.add(utf8.encode(mockCompilerOutput));
+  });
+  final CompilerOutput output = await generator.reject();
+  expect(output, isNull);
+  final String commands = mockFrontendServerStdIn.getAndClear();
+  final RegExp re = RegExp(expected);
+  expect(commands, matches(re));
+  mockFrontendServerStdIn._stdInWrites.clear();
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+class MockStream extends Mock implements Stream<List<int>> {}
+class MockStdIn extends Mock implements IOSink {
+  final StringBuffer _stdInWrites = StringBuffer();
+
+  String getAndClear() {
+    final String result = _stdInWrites.toString();
+    _stdInWrites.clear();
+    return result;
+  }
+
+  @override
+  void write([ Object o = '' ]) {
+    _stdInWrites.write(o);
+  }
+
+  @override
+  void writeln([ Object o = '' ]) {
+    _stdInWrites.writeln(o);
+  }
+}
+class MockFileSystem extends Mock implements FileSystem {}
+class MockFile extends Mock implements File {}
diff --git a/packages/flutter_tools/test/general.shard/config_test.dart b/packages/flutter_tools/test/general.shard/config_test.dart
new file mode 100644
index 0000000..8217375
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/config_test.dart
@@ -0,0 +1,48 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import '../src/common.dart';
+
+void main() {
+  Config config;
+  Directory tempDir;
+
+  setUp(() {
+    tempDir = fs.systemTempDirectory.createTempSync('flutter_config_test.');
+    final File file = fs.file(fs.path.join(tempDir.path, '.settings'));
+    config = Config(file);
+  });
+
+  tearDown(() {
+    tryToDelete(tempDir);
+  });
+
+  group('config', () {
+    test('get set value', () async {
+      expect(config.getValue('foo'), null);
+      config.setValue('foo', 'bar');
+      expect(config.getValue('foo'), 'bar');
+      expect(config.keys, contains('foo'));
+    });
+
+    test('containsKey', () async {
+      expect(config.containsKey('foo'), false);
+      config.setValue('foo', 'bar');
+      expect(config.containsKey('foo'), true);
+    });
+
+    test('removeValue', () async {
+      expect(config.getValue('foo'), null);
+      config.setValue('foo', 'bar');
+      expect(config.getValue('foo'), 'bar');
+      expect(config.keys, contains('foo'));
+      config.removeValue('foo');
+      expect(config.getValue('foo'), null);
+      expect(config.keys, isNot(contains('foo')));
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart
new file mode 100644
index 0000000..1d277c0
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart
@@ -0,0 +1,51 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/test/coverage_collector.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+
+void main() {
+  MockVMService mockVMService;
+
+  setUp(() {
+    mockVMService = MockVMService();
+  });
+
+  test('Coverage collector Can handle coverage sentinenl data', () async {
+    when(mockVMService.vm.isolates.first.invokeRpcRaw('getScripts', params: anyNamed('params')))
+      .thenAnswer((Invocation invocation) async {
+        return <String, Object>{'type': 'Sentinel', 'kind': 'Collected', 'valueAsString': '<collected>'};
+      });
+    final Map<String, Object> result = await collect(null, (String predicate) => true, connector: (Uri uri) async {
+      return mockVMService;
+    });
+
+    expect(result, <String, Object>{'type': 'CodeCoverage', 'coverage': <Object>[]});
+  });
+}
+
+class MockVMService extends Mock implements VMService {
+  @override
+  final MockVM vm = MockVM();
+}
+
+class MockVM extends Mock implements VM {
+  @override
+  final List<MockIsolate> isolates = <MockIsolate>[ MockIsolate() ];
+}
+
+class MockIsolate extends Mock implements Isolate {}
+
+class MockProcess extends Mock implements Process {
+  final Completer<int>completer = Completer<int>();
+
+  @override
+  Future<int> get exitCode => completer.future;
+}
diff --git a/packages/flutter_tools/test/general.shard/crash_reporting_test.dart b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
new file mode 100644
index 0000000..e9de43f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/crash_reporting_test.dart
@@ -0,0 +1,326 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+
+import 'package:flutter_tools/runner.dart' as tools;
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/crash_reporting.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:pedantic/pedantic.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('crash reporting', () {
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() async {
+      tools.crashFileSystem = MemoryFileSystem();
+      setExitFunctionForTests((_) { });
+    });
+
+    tearDown(() {
+      tools.crashFileSystem = const LocalFileSystem();
+      restoreExitFunction();
+    });
+
+    testUsingContext('should send crash reports', () async {
+      final RequestInfo requestInfo = RequestInfo();
+
+      CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
+      final int exitCode = await tools.run(
+        <String>['crash'],
+        <FlutterCommand>[_CrashCommand()],
+        reportCrashes: true,
+        flutterVersion: 'test-version',
+      );
+      expect(exitCode, 1);
+
+      await verifyCrashReportSent(requestInfo);
+    }, overrides: <Type, Generator>{
+      Stdio: () => const _NoStderr(),
+    });
+
+    testUsingContext('should send crash reports when async throws', () async {
+      final Completer<int> exitCodeCompleter = Completer<int>();
+      setExitFunctionForTests((int exitCode) {
+        exitCodeCompleter.complete(exitCode);
+      });
+
+      final RequestInfo requestInfo = RequestInfo();
+
+      CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
+
+      unawaited(tools.run(
+        <String>['crash'],
+        <FlutterCommand>[_CrashAsyncCommand()],
+        reportCrashes: true,
+        flutterVersion: 'test-version',
+      ));
+      expect(await exitCodeCompleter.future, equals(1));
+      await verifyCrashReportSent(requestInfo);
+    }, overrides: <Type, Generator>{
+      Stdio: () => const _NoStderr(),
+    });
+
+    testUsingContext('should not send a crash report if on a user-branch', () async {
+      String method;
+      Uri uri;
+
+      CrashReportSender.initializeWith(MockClient((Request request) async {
+        method = request.method;
+        uri = request.url;
+
+        return Response(
+          'test-report-id',
+          200,
+        );
+      }));
+
+      final int exitCode = await tools.run(
+        <String>['crash'],
+        <FlutterCommand>[_CrashCommand()],
+        reportCrashes: true,
+        flutterVersion: '[user-branch]/v1.2.3',
+      );
+
+      expect(exitCode, 1);
+
+      // Verify that the report wasn't sent
+      expect(method, null);
+      expect(uri, null);
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.statusText, '');
+    }, overrides: <Type, Generator>{
+      Stdio: () => const _NoStderr(),
+    });
+
+    testUsingContext('can override base URL', () async {
+      Uri uri;
+      CrashReportSender.initializeWith(MockClient((Request request) async {
+        uri = request.url;
+        return Response('test-report-id', 200);
+      }));
+
+      final int exitCode = await tools.run(
+        <String>['crash'],
+        <FlutterCommand>[_CrashCommand()],
+        reportCrashes: true,
+        flutterVersion: 'test-version',
+      );
+
+      expect(exitCode, 1);
+
+      // Verify that we sent the crash report.
+      expect(uri, isNotNull);
+      expect(uri, Uri(
+        scheme: 'https',
+        host: 'localhost',
+        port: 12345,
+        path: '/fake_server',
+        queryParameters: <String, String>{
+          'product': 'Flutter_Tools',
+          'version': 'test-version',
+        },
+      ));
+    }, overrides: <Type, Generator>{
+      Platform: () => FakePlatform(
+        operatingSystem: 'linux',
+        environment: <String, String>{
+          'HOME': '/',
+          'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
+        },
+        script: Uri(scheme: 'data'),
+      ),
+      Stdio: () => const _NoStderr(),
+    });
+  });
+}
+
+class RequestInfo {
+  String method;
+  Uri uri;
+  Map<String, String> fields;
+}
+
+Future<void> verifyCrashReportSent(RequestInfo crashInfo) async {
+  // Verify that we sent the crash report.
+  expect(crashInfo.method, 'POST');
+  expect(crashInfo.uri, Uri(
+    scheme: 'https',
+    host: 'clients2.google.com',
+    port: 443,
+    path: '/cr/report',
+    queryParameters: <String, String>{
+      'product': 'Flutter_Tools',
+      'version': 'test-version',
+    },
+  ));
+  expect(crashInfo.fields['uuid'], '00000000-0000-4000-0000-000000000000');
+  expect(crashInfo.fields['product'], 'Flutter_Tools');
+  expect(crashInfo.fields['version'], 'test-version');
+  expect(crashInfo.fields['osName'], platform.operatingSystem);
+  expect(crashInfo.fields['osVersion'], 'fake OS name and version');
+  expect(crashInfo.fields['type'], 'DartError');
+  expect(crashInfo.fields['error_runtime_type'], 'StateError');
+  expect(crashInfo.fields['error_message'], 'Bad state: Test bad state error');
+
+  final BufferLogger logger = context.get<Logger>();
+  expect(logger.statusText, 'Sending crash report to Google.\n'
+      'Crash report sent (report ID: test-report-id)\n');
+
+  // Verify that we've written the crash report to disk.
+  final List<String> writtenFiles =
+  (await tools.crashFileSystem.directory('/').list(recursive: true).toList())
+      .map((FileSystemEntity e) => e.path).toList();
+  expect(writtenFiles, hasLength(1));
+  expect(writtenFiles, contains('flutter_01.log'));
+}
+
+class MockCrashReportSender extends MockClient {
+  MockCrashReportSender(RequestInfo crashInfo) : super((Request request) async {
+      crashInfo.method = request.method;
+      crashInfo.uri = request.url;
+
+      // A very ad-hoc multipart request parser. Good enough for this test.
+      String boundary = request.headers['Content-Type'];
+      boundary = boundary.substring(boundary.indexOf('boundary=') + 9);
+      crashInfo.fields = Map<String, String>.fromIterable(
+        utf8.decode(request.bodyBytes)
+            .split('--$boundary')
+            .map<List<String>>((String part) {
+          final Match nameMatch = RegExp(r'name="(.*)"').firstMatch(part);
+          if (nameMatch == null)
+            return null;
+          final String name = nameMatch[1];
+          final String value = part.split('\n').skip(2).join('\n').trim();
+          return <String>[name, value];
+        })
+            .where((List<String> pair) => pair != null),
+        key: (dynamic key) {
+          final List<String> pair = key;
+          return pair[0];
+        },
+        value: (dynamic value) {
+          final List<String> pair = value;
+          return pair[1];
+        },
+      );
+
+      return Response(
+        'test-report-id',
+        200,
+      );
+    });
+}
+
+/// Throws a random error to simulate a CLI crash.
+class _CrashCommand extends FlutterCommand {
+
+  @override
+  String get description => 'Simulates a crash';
+
+  @override
+  String get name => 'crash';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    void fn1() {
+      throw StateError('Test bad state error');
+    }
+
+    void fn2() {
+      fn1();
+    }
+
+    void fn3() {
+      fn2();
+    }
+
+    fn3();
+
+    return null;
+  }
+}
+
+/// Throws StateError from async callback.
+class _CrashAsyncCommand extends FlutterCommand {
+
+  @override
+  String get description => 'Simulates a crash';
+
+  @override
+  String get name => 'crash';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    Timer.run(() {
+      throw StateError('Test bad state error');
+    });
+    return Completer<FlutterCommandResult>().future; // expect StateError
+  }
+}
+
+class _NoStderr extends Stdio {
+  const _NoStderr();
+
+  @override
+  IOSink get stderr => const _NoopIOSink();
+}
+
+class _NoopIOSink implements IOSink {
+  const _NoopIOSink();
+
+  @override
+  Encoding get encoding => utf8;
+
+  @override
+  set encoding(_) => throw UnsupportedError('');
+
+  @override
+  void add(_) { }
+
+  @override
+  void write(_) { }
+
+  @override
+  void writeAll(_, [ __ = '' ]) { }
+
+  @override
+  void writeln([ _ = '' ]) { }
+
+  @override
+  void writeCharCode(_) { }
+
+  @override
+  void addError(_, [ __ ]) { }
+
+  @override
+  Future<dynamic> addStream(_) async { }
+
+  @override
+  Future<dynamic> flush() async { }
+
+  @override
+  Future<dynamic> close() async { }
+
+  @override
+  Future<dynamic> get done async { }
+}
diff --git a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
new file mode 100644
index 0000000..48093ca
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
@@ -0,0 +1,268 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/dart/pub.dart';
+
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:quiver/testing/async.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  setUpAll(() {
+    Cache.flutterRoot = getFlutterRoot();
+  });
+
+  testUsingContext('pub get 69', () async {
+    String error;
+
+    final MockProcessManager processMock = context.get<ProcessManager>();
+
+    FakeAsync().run((FakeAsync time) {
+      expect(processMock.lastPubEnvironment, isNull);
+      expect(testLogger.statusText, '');
+      pubGet(context: PubContext.flutterTests, checkLastModified: false).then((void value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic thrownError) {
+        error = 'test failed unexpectedly: $thrownError';
+      });
+      time.elapse(const Duration(milliseconds: 500));
+      expect(testLogger.statusText,
+        'Running "flutter pub get" in /...\n'
+        'pub get failed (69) -- attempting retry 1 in 1 second...\n',
+      );
+      expect(processMock.lastPubEnvironment, contains('flutter_cli:flutter_tests'));
+      expect(processMock.lastPubCache, isNull);
+      time.elapse(const Duration(milliseconds: 500));
+      expect(testLogger.statusText,
+        'Running "flutter pub get" in /...\n'
+        'pub get failed (69) -- attempting retry 1 in 1 second...\n'
+        'pub get failed (69) -- attempting retry 2 in 2 seconds...\n',
+      );
+      time.elapse(const Duration(seconds: 1));
+      expect(testLogger.statusText,
+        'Running "flutter pub get" in /...\n'
+        'pub get failed (69) -- attempting retry 1 in 1 second...\n'
+        'pub get failed (69) -- attempting retry 2 in 2 seconds...\n',
+      );
+      time.elapse(const Duration(seconds: 100)); // from t=0 to t=100
+      expect(testLogger.statusText,
+        'Running "flutter pub get" in /...\n'
+        'pub get failed (69) -- attempting retry 1 in 1 second...\n'
+        'pub get failed (69) -- attempting retry 2 in 2 seconds...\n'
+        'pub get failed (69) -- attempting retry 3 in 4 seconds...\n' // at t=1
+        'pub get failed (69) -- attempting retry 4 in 8 seconds...\n' // at t=5
+        'pub get failed (69) -- attempting retry 5 in 16 seconds...\n' // at t=13
+        'pub get failed (69) -- attempting retry 6 in 32 seconds...\n' // at t=29
+        'pub get failed (69) -- attempting retry 7 in 64 seconds...\n', // at t=61
+      );
+      time.elapse(const Duration(seconds: 200)); // from t=0 to t=200
+      expect(testLogger.statusText,
+        'Running "flutter pub get" in /...\n'
+        'pub get failed (69) -- attempting retry 1 in 1 second...\n'
+        'pub get failed (69) -- attempting retry 2 in 2 seconds...\n'
+        'pub get failed (69) -- attempting retry 3 in 4 seconds...\n'
+        'pub get failed (69) -- attempting retry 4 in 8 seconds...\n'
+        'pub get failed (69) -- attempting retry 5 in 16 seconds...\n'
+        'pub get failed (69) -- attempting retry 6 in 32 seconds...\n'
+        'pub get failed (69) -- attempting retry 7 in 64 seconds...\n'
+        'pub get failed (69) -- attempting retry 8 in 64 seconds...\n' // at t=39
+        'pub get failed (69) -- attempting retry 9 in 64 seconds...\n' // at t=103
+        'pub get failed (69) -- attempting retry 10 in 64 seconds...\n', // at t=167
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => MockProcessManager(69),
+    FileSystem: () => MockFileSystem(),
+    Platform: () => FakePlatform(
+      environment: <String, String>{},
+    ),
+  });
+
+  testUsingContext('pub cache in root is used', () async {
+    String error;
+
+    final MockProcessManager processMock = context.get<ProcessManager>() as MockProcessManager;
+    final MockFileSystem fsMock = context.get<FileSystem>() as MockFileSystem;
+
+    FakeAsync().run((FakeAsync time) {
+      MockDirectory.findCache = true;
+      expect(processMock.lastPubEnvironment, isNull);
+      expect(processMock.lastPubCache, isNull);
+      pubGet(context: PubContext.flutterTests, checkLastModified: false).then((void value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic thrownError) {
+        error = 'test failed unexpectedly: $thrownError';
+      });
+      time.elapse(const Duration(milliseconds: 500));
+      expect(processMock.lastPubCache, equals(fsMock.path.join(Cache.flutterRoot, '.pub-cache')));
+      expect(error, isNull);
+    });
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => MockProcessManager(69),
+    FileSystem: () => MockFileSystem(),
+    Platform: () => FakePlatform(
+      environment: <String, String>{},
+    ),
+  });
+
+  testUsingContext('pub cache in environment is used', () async {
+    String error;
+
+    final MockProcessManager processMock = context.get<ProcessManager>();
+
+    FakeAsync().run((FakeAsync time) {
+      MockDirectory.findCache = true;
+      expect(processMock.lastPubEnvironment, isNull);
+      expect(processMock.lastPubCache, isNull);
+      pubGet(context: PubContext.flutterTests, checkLastModified: false).then((void value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic thrownError) {
+        error = 'test failed unexpectedly: $thrownError';
+      });
+      time.elapse(const Duration(milliseconds: 500));
+      expect(processMock.lastPubCache, equals('custom/pub-cache/path'));
+      expect(error, isNull);
+    });
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => MockProcessManager(69),
+    FileSystem: () => MockFileSystem(),
+    Platform: () => FakePlatform(
+      environment: <String, String>{'PUB_CACHE': 'custom/pub-cache/path'},
+    ),
+  });
+}
+
+typedef StartCallback = void Function(List<dynamic> command);
+
+class MockProcessManager implements ProcessManager {
+  MockProcessManager(this.fakeExitCode);
+
+  final int fakeExitCode;
+
+  String lastPubEnvironment;
+  String lastPubCache;
+
+  @override
+  Future<Process> start(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    ProcessStartMode mode = ProcessStartMode.normal,
+  }) {
+    lastPubEnvironment = environment['PUB_ENVIRONMENT'];
+    lastPubCache = environment['PUB_CACHE'];
+    return Future<Process>.value(MockProcess(fakeExitCode));
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+class MockProcess implements Process {
+  MockProcess(this.fakeExitCode);
+
+  final int fakeExitCode;
+
+  @override
+  Stream<List<int>> get stdout => MockStream<List<int>>();
+
+  @override
+  Stream<List<int>> get stderr => MockStream<List<int>>();
+
+  @override
+  Future<int> get exitCode => Future<int>.value(fakeExitCode);
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+class MockStream<T> implements Stream<T> {
+  @override
+  Stream<S> transform<S>(StreamTransformer<T, S> streamTransformer) => MockStream<S>();
+
+  @override
+  Stream<T> where(bool test(T event)) => MockStream<T>();
+
+  @override
+  StreamSubscription<T> listen(void onData(T event), { Function onError, void onDone(), bool cancelOnError }) {
+    return MockStreamSubscription<T>();
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+class MockStreamSubscription<T> implements StreamSubscription<T> {
+  @override
+  Future<E> asFuture<E>([ E futureValue ]) => Future<E>.value();
+
+  @override
+  Future<void> cancel() async { }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+
+class MockFileSystem extends ForwardingFileSystem {
+  MockFileSystem() : super(MemoryFileSystem());
+
+  @override
+  File file(dynamic path) {
+    return MockFile();
+  }
+
+  @override
+  Directory directory(dynamic path) {
+    return MockDirectory(path);
+  }
+}
+
+class MockFile implements File {
+  @override
+  Future<RandomAccessFile> open({ FileMode mode = FileMode.read }) async {
+    return MockRandomAccessFile();
+  }
+
+  @override
+  bool existsSync() => true;
+
+  @override
+  DateTime lastModifiedSync() => DateTime(0);
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+class MockDirectory implements Directory {
+  MockDirectory(this.path);
+
+  @override
+  final String path;
+
+  static bool findCache = false;
+
+  @override
+  bool existsSync() => findCache && path.endsWith('.pub-cache');
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => null;
+}
+
+class MockRandomAccessFile extends Mock implements RandomAccessFile {}
diff --git a/packages/flutter_tools/test/general.shard/devfs_test.dart b/packages/flutter_tools/test/general.shard/devfs_test.dart
new file mode 100644
index 0000000..7a1a112
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/devfs_test.dart
@@ -0,0 +1,296 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io'; // ignore: dart_io_import
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/devfs.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  FileSystem fs;
+  String filePath;
+  Directory tempDir;
+  String basePath;
+  DevFS devFS;
+
+  setUpAll(() {
+    fs = MemoryFileSystem();
+    filePath = fs.path.join('lib', 'foo.txt');
+  });
+
+  group('DevFSContent', () {
+    test('bytes', () {
+      final DevFSByteContent content = DevFSByteContent(<int>[4, 5, 6]);
+      expect(content.bytes, orderedEquals(<int>[4, 5, 6]));
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+      content.bytes = <int>[7, 8, 9, 2];
+      expect(content.bytes, orderedEquals(<int>[7, 8, 9, 2]));
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+    });
+    test('string', () {
+      final DevFSStringContent content = DevFSStringContent('some string');
+      expect(content.string, 'some string');
+      expect(content.bytes, orderedEquals(utf8.encode('some string')));
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+      content.string = 'another string';
+      expect(content.string, 'another string');
+      expect(content.bytes, orderedEquals(utf8.encode('another string')));
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+      content.bytes = utf8.encode('foo bar');
+      expect(content.string, 'foo bar');
+      expect(content.bytes, orderedEquals(utf8.encode('foo bar')));
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+    });
+    testUsingContext('file', () async {
+      final File file = fs.file(filePath);
+      final DevFSFileContent content = DevFSFileContent(file);
+      expect(content.isModified, isFalse);
+      expect(content.isModified, isFalse);
+
+      file.parent.createSync(recursive: true);
+      file.writeAsBytesSync(<int>[1, 2, 3], flush: true);
+
+      final DateTime fiveSecondsAgo = DateTime.now().subtract(const Duration(seconds:5));
+      expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
+      expect(content.isModifiedAfter(fiveSecondsAgo), isTrue);
+      expect(content.isModifiedAfter(null), isTrue);
+
+      file.writeAsBytesSync(<int>[2, 3, 4], flush: true);
+      expect(content.fileDependencies, <String>[filePath]);
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+      expect(await content.contentsAsBytes(), <int>[2, 3, 4]);
+      updateFileModificationTime(file.path, fiveSecondsAgo, 0);
+      expect(content.isModified, isFalse);
+      expect(content.isModified, isFalse);
+
+      file.deleteSync();
+      expect(content.isModified, isTrue);
+      expect(content.isModified, isFalse);
+      expect(content.isModified, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    }, skip: Platform.isWindows); // TODO(jonahwilliams): fix or disable this functionality.
+  });
+
+  group('devfs remote', () {
+    MockVMService vmService;
+    final MockResidentCompiler residentCompiler = MockResidentCompiler();
+
+    setUpAll(() async {
+      tempDir = _newTempDir(fs);
+      basePath = tempDir.path;
+      vmService = MockVMService();
+      await vmService.setUp();
+    });
+    tearDownAll(() async {
+      await vmService.tearDown();
+      _cleanupTempDirs();
+    });
+
+    testUsingContext('create dev file system', () async {
+      // simulate workspace
+      final File file = fs.file(fs.path.join(basePath, filePath));
+      await file.parent.create(recursive: true);
+      file.writeAsBytesSync(<int>[1, 2, 3]);
+
+      // simulate package
+      await _createPackage(fs, 'somepkg', 'somefile.txt');
+
+      devFS = DevFS(vmService, 'test', tempDir);
+      await devFS.create();
+      vmService.expectMessages(<String>['create test']);
+      expect(devFS.assetPathsToEvict, isEmpty);
+
+      final UpdateFSReport report = await devFS.update(
+        mainPath: 'lib/foo.txt',
+        generator: residentCompiler,
+        pathToReload: 'lib/foo.txt.dill',
+        trackWidgetCreation: false,
+        invalidatedFiles: <Uri>[],
+      );
+      vmService.expectMessages(<String>[
+        'writeFile test lib/foo.txt.dill',
+      ]);
+      expect(devFS.assetPathsToEvict, isEmpty);
+      expect(report.syncedBytes, 22);
+      expect(report.success, true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('delete dev file system', () async {
+      expect(vmService.messages, isEmpty, reason: 'prior test timeout');
+      await devFS.destroy();
+      vmService.expectMessages(<String>['destroy test']);
+      expect(devFS.assetPathsToEvict, isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('cleanup preexisting file system', () async {
+      // simulate workspace
+      final File file = fs.file(fs.path.join(basePath, filePath));
+      await file.parent.create(recursive: true);
+      file.writeAsBytesSync(<int>[1, 2, 3]);
+
+      // simulate package
+      await _createPackage(fs, 'somepkg', 'somefile.txt');
+
+      devFS = DevFS(vmService, 'test', tempDir);
+      await devFS.create();
+      vmService.expectMessages(<String>['create test']);
+      expect(devFS.assetPathsToEvict, isEmpty);
+
+      // Try to create again.
+      await devFS.create();
+      vmService.expectMessages(<String>['create test', 'destroy test', 'create test']);
+      expect(devFS.assetPathsToEvict, isEmpty);
+
+      // Really destroy.
+      await devFS.destroy();
+      vmService.expectMessages(<String>['destroy test']);
+      expect(devFS.assetPathsToEvict, isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+  });
+}
+
+class MockVMService extends BasicMock implements VMService {
+  MockVMService() {
+    _vm = MockVM(this);
+  }
+
+  Uri _httpAddress;
+  HttpServer _server;
+  MockVM _vm;
+
+  @override
+  Uri get httpAddress => _httpAddress;
+
+  @override
+  VM get vm => _vm;
+
+  Future<void> setUp() async {
+    try {
+      _server = await HttpServer.bind(InternetAddress.loopbackIPv6, 0);
+      _httpAddress = Uri.parse('http://[::1]:${_server.port}');
+    } on SocketException {
+      // Fall back to IPv4 if the host doesn't support binding to IPv6 localhost
+      _server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
+      _httpAddress = Uri.parse('http://127.0.0.1:${_server.port}');
+    }
+    _server.listen((HttpRequest request) {
+      final String fsName = request.headers.value('dev_fs_name');
+      final String devicePath = utf8.decode(base64.decode(request.headers.value('dev_fs_uri_b64')));
+      messages.add('writeFile $fsName $devicePath');
+      request.drain<List<int>>().then<void>((List<int> value) {
+        request.response
+          ..write('Got it')
+          ..close();
+      });
+    });
+  }
+
+  Future<void> tearDown() async {
+    await _server?.close();
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+class MockVM implements VM {
+  MockVM(this._service);
+
+  final MockVMService _service;
+  final Uri _baseUri = Uri.parse('file:///tmp/devfs/test');
+  bool _devFSExists = false;
+
+  static const int kFileSystemAlreadyExists = 1001;
+
+  @override
+  Future<Map<String, dynamic>> createDevFS(String fsName) async {
+    _service.messages.add('create $fsName');
+    if (_devFSExists) {
+      throw rpc.RpcException(kFileSystemAlreadyExists, 'File system already exists');
+    }
+    _devFSExists = true;
+    return <String, dynamic>{'uri': '$_baseUri'};
+  }
+
+  @override
+  Future<Map<String, dynamic>> deleteDevFS(String fsName) async {
+    _service.messages.add('destroy $fsName');
+    _devFSExists = false;
+    return <String, dynamic>{'type': 'Success'};
+  }
+
+  @override
+  Future<Map<String, dynamic>> invokeRpcRaw(
+    String method, {
+    Map<String, dynamic> params = const <String, dynamic>{},
+    Duration timeout,
+    bool timeoutFatal = true,
+    bool truncateLogs = true,
+  }) async {
+    _service.messages.add('$method $params');
+    return <String, dynamic>{'success': true};
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
+
+
+final List<Directory> _tempDirs = <Directory>[];
+final Map <String, Uri> _packages = <String, Uri>{};
+
+Directory _newTempDir(FileSystem fs) {
+  final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_devfs${_tempDirs.length}_test.');
+  _tempDirs.add(tempDir);
+  return tempDir;
+}
+
+void _cleanupTempDirs() {
+  while (_tempDirs.isNotEmpty)
+    tryToDelete(_tempDirs.removeLast());
+}
+
+Future<void> _createPackage(FileSystem fs, String pkgName, String pkgFileName, { bool doubleSlash = false }) async {
+  final Directory pkgTempDir = _newTempDir(fs);
+  String pkgFilePath = fs.path.join(pkgTempDir.path, pkgName, 'lib', pkgFileName);
+  if (doubleSlash) {
+    // Force two separators into the path.
+    final String doubleSlash = fs.path.separator + fs.path.separator;
+    pkgFilePath = pkgTempDir.path + doubleSlash + fs.path.join(pkgName, 'lib', pkgFileName);
+  }
+  final File pkgFile = fs.file(pkgFilePath);
+  await pkgFile.parent.create(recursive: true);
+  pkgFile.writeAsBytesSync(<int>[11, 12, 13]);
+  _packages[pkgName] = fs.path.toUri(pkgFile.parent.path);
+  final StringBuffer sb = StringBuffer();
+  _packages.forEach((String pkgName, Uri pkgUri) {
+    sb.writeln('$pkgName:$pkgUri');
+  });
+  fs.file(fs.path.join(_tempDirs[0].path, '.packages')).writeAsStringSync(sb.toString());
+}
+
diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart
new file mode 100644
index 0000000..e63b01c
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/device_test.dart
@@ -0,0 +1,154 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/project.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('DeviceManager', () {
+    testUsingContext('getDevices', () async {
+      // Test that DeviceManager.getDevices() doesn't throw.
+      final DeviceManager deviceManager = DeviceManager();
+      final List<Device> devices = await deviceManager.getDevices().toList();
+      expect(devices, isList);
+    });
+
+    testUsingContext('getDeviceById', () async {
+      final _MockDevice device1 = _MockDevice('Nexus 5', '0553790d0a4e726f');
+      final _MockDevice device2 = _MockDevice('Nexus 5X', '01abfc49119c410e');
+      final _MockDevice device3 = _MockDevice('iPod touch', '82564b38861a9a5');
+      final List<Device> devices = <Device>[device1, device2, device3];
+      final DeviceManager deviceManager = TestDeviceManager(devices);
+
+      Future<void> expectDevice(String id, List<Device> expected) async {
+        expect(await deviceManager.getDevicesById(id).toList(), expected);
+      }
+      await expectDevice('01abfc49119c410e', <Device>[device2]);
+      await expectDevice('Nexus 5X', <Device>[device2]);
+      await expectDevice('0553790d0a4e726f', <Device>[device1]);
+      await expectDevice('Nexus 5', <Device>[device1]);
+      await expectDevice('0553790', <Device>[device1]);
+      await expectDevice('Nexus', <Device>[device1, device2]);
+    });
+  });
+
+  group('Filter devices', () {
+    _MockDevice ephemeral;
+    _MockDevice nonEphemeralOne;
+    _MockDevice nonEphemeralTwo;
+    _MockDevice unsupported;
+    _MockDevice webDevice;
+    _MockDevice fuchsiaDevice;
+
+    setUp(() {
+      ephemeral = _MockDevice('ephemeral', 'ephemeral', true);
+      nonEphemeralOne = _MockDevice('nonEphemeralOne', 'nonEphemeralOne', false);
+      nonEphemeralTwo = _MockDevice('nonEphemeralTwo', 'nonEphemeralTwo', false);
+      unsupported = _MockDevice('unsupported', 'unsupported', true, false);
+      webDevice = _MockDevice('webby', 'webby')
+        ..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.web_javascript);
+      fuchsiaDevice = _MockDevice('fuchsiay', 'fuchsiay')
+        ..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.fuchsia);
+    });
+
+    testUsingContext('chooses ephemeral device', () async {
+      final List<Device> devices = <Device>[
+        ephemeral,
+        nonEphemeralOne,
+        nonEphemeralTwo,
+        unsupported,
+      ];
+
+      final DeviceManager deviceManager = TestDeviceManager(devices);
+      final List<Device> filtered = await deviceManager.findTargetDevices(FlutterProject.current());
+
+      expect(filtered.single, ephemeral);
+    });
+
+    testUsingContext('does not remove all non-ephemeral', () async {
+      final List<Device> devices = <Device>[
+        nonEphemeralOne,
+        nonEphemeralTwo,
+      ];
+
+      final DeviceManager deviceManager = TestDeviceManager(devices);
+      final List<Device> filtered = await deviceManager.findTargetDevices(FlutterProject.current());
+
+      expect(filtered, <Device>[
+        nonEphemeralOne,
+        nonEphemeralTwo,
+      ]);
+    });
+
+    testUsingContext('Removes web and fuchsia from --all', () async {
+      final List<Device> devices = <Device>[
+        webDevice,
+        fuchsiaDevice,
+      ];
+      final DeviceManager deviceManager = TestDeviceManager(devices);
+      deviceManager.specifiedDeviceId = 'all';
+
+      final List<Device> filtered = await deviceManager.findTargetDevices(FlutterProject.current());
+
+      expect(filtered, <Device>[]);
+    });
+
+    testUsingContext('Removes unsupported devices from --all', () async {
+      final List<Device> devices = <Device>[
+        nonEphemeralOne,
+        nonEphemeralTwo,
+        unsupported,
+      ];
+      final DeviceManager deviceManager = TestDeviceManager(devices);
+      deviceManager.specifiedDeviceId = 'all';
+
+      final List<Device> filtered = await deviceManager.findTargetDevices(FlutterProject.current());
+
+      expect(filtered, <Device>[
+        nonEphemeralOne,
+        nonEphemeralTwo,
+      ]);
+    });
+  });
+}
+
+class TestDeviceManager extends DeviceManager {
+  TestDeviceManager(this.allDevices);
+
+  final List<Device> allDevices;
+
+  @override
+  Stream<Device> getAllConnectedDevices() {
+    return Stream<Device>.fromIterable(allDevices);
+  }
+}
+
+class _MockDevice extends Device {
+  _MockDevice(this.name, String id, [bool ephemeral = true, this._isSupported = true]) : super(
+      id,
+      platformType: PlatformType.web,
+      category: Category.mobile,
+      ephemeral: ephemeral,
+  );
+
+  final bool _isSupported;
+
+  @override
+  final String name;
+
+  @override
+  Future<TargetPlatform> targetPlatform = Future<TargetPlatform>.value(TargetPlatform.android_arm);
+
+  @override
+  void noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+
+  @override
+  bool isSupportedForProject(FlutterProject flutterProject) => _isSupported;
+}
diff --git a/packages/flutter_tools/test/general.shard/emulator_test.dart b/packages/flutter_tools/test/general.shard/emulator_test.dart
new file mode 100644
index 0000000..7e6d389
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/emulator_test.dart
@@ -0,0 +1,262 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:collection/collection.dart' show ListEquality;
+import 'package:flutter_tools/src/android/android_sdk.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/emulator.dart';
+import 'package:flutter_tools/src/ios/ios_emulators.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  MockProcessManager mockProcessManager;
+  MockConfig mockConfig;
+  MockAndroidSdk mockSdk;
+  MockXcode mockXcode;
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+    mockConfig = MockConfig();
+    mockSdk = MockAndroidSdk();
+    mockXcode = MockXcode();
+
+    when(mockSdk.avdManagerPath).thenReturn('avdmanager');
+    when(mockSdk.emulatorPath).thenReturn('emulator');
+  });
+
+  group('EmulatorManager', () {
+    testUsingContext('getEmulators', () async {
+      // Test that EmulatorManager.getEmulators() doesn't throw.
+      final List<Emulator> emulators =
+          await emulatorManager.getAllAvailableEmulators();
+      expect(emulators, isList);
+    });
+
+    testUsingContext('getEmulatorsById', () async {
+      final _MockEmulator emulator1 =
+          _MockEmulator('Nexus_5', 'Nexus 5', 'Google');
+      final _MockEmulator emulator2 =
+          _MockEmulator('Nexus_5X_API_27_x86', 'Nexus 5X', 'Google');
+      final _MockEmulator emulator3 =
+          _MockEmulator('iOS Simulator', 'iOS Simulator', 'Apple');
+      final List<Emulator> emulators = <Emulator>[
+        emulator1,
+        emulator2,
+        emulator3,
+      ];
+      final TestEmulatorManager testEmulatorManager =
+          TestEmulatorManager(emulators);
+
+      Future<void> expectEmulator(String id, List<Emulator> expected) async {
+        expect(await testEmulatorManager.getEmulatorsMatching(id), expected);
+      }
+
+      await expectEmulator('Nexus_5', <Emulator>[emulator1]);
+      await expectEmulator('Nexus_5X', <Emulator>[emulator2]);
+      await expectEmulator('Nexus_5X_API_27_x86', <Emulator>[emulator2]);
+      await expectEmulator('Nexus', <Emulator>[emulator1, emulator2]);
+      await expectEmulator('iOS Simulator', <Emulator>[emulator3]);
+      await expectEmulator('ios', <Emulator>[emulator3]);
+    });
+
+    testUsingContext('create emulator with an empty name does not fail', () async {
+      final CreateEmulatorResult res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext('create emulator with a unique name does not throw', () async {
+      final CreateEmulatorResult res =
+          await emulatorManager.createEmulator(name: 'test');
+      expect(res.success, equals(true));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext('create emulator with an existing name errors', () async {
+      final CreateEmulatorResult res =
+          await emulatorManager.createEmulator(name: 'existing-avd-1');
+      expect(res.success, equals(false));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+
+    testUsingContext('create emulator without a name but when default exists adds a suffix', () async {
+      // First will get default name.
+      CreateEmulatorResult res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+
+      final String defaultName = res.emulatorName;
+
+      // Second...
+      res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+      expect(res.emulatorName, equals('${defaultName}_2'));
+
+      // Third...
+      res = await emulatorManager.createEmulator();
+      expect(res.success, equals(true));
+      expect(res.emulatorName, equals('${defaultName}_3'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AndroidSdk: () => mockSdk,
+    });
+  });
+
+  group('ios_emulators', () {
+    bool didAttemptToRunSimulator = false;
+    setUp(() {
+      when(mockXcode.xcodeSelectPath).thenReturn('/fake/Xcode.app/Contents/Developer');
+      when(mockXcode.getSimulatorPath()).thenAnswer((_) => '/fake/simulator.app');
+      when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+        final List<String> args = invocation.positionalArguments[0];
+        if (args.length >= 3 && args[0] == 'open' && args[1] == '-a' && args[2] == '/fake/simulator.app') {
+          didAttemptToRunSimulator = true;
+        }
+        return ProcessResult(101, 0, '', '');
+      });
+    });
+    testUsingContext('runs correct launch commands', () async {
+      final Emulator emulator = IOSEmulator('ios');
+      await emulator.launch();
+      expect(didAttemptToRunSimulator, equals(true));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      Xcode: () => mockXcode,
+    });
+  });
+}
+
+class TestEmulatorManager extends EmulatorManager {
+  TestEmulatorManager(this.allEmulators);
+
+  final List<Emulator> allEmulators;
+
+  @override
+  Future<List<Emulator>> getAllAvailableEmulators() {
+    return Future<List<Emulator>>.value(allEmulators);
+  }
+}
+
+class _MockEmulator extends Emulator {
+  _MockEmulator(String id, this.name, this.manufacturer)
+    : super(id, true);
+
+  @override
+  final String name;
+
+  @override
+  final String manufacturer;
+
+  @override
+  Category get category => Category.mobile;
+
+  @override
+  PlatformType get platformType => PlatformType.android;
+
+  @override
+  Future<void> launch() {
+    throw UnimplementedError('Not implemented in Mock');
+  }
+}
+
+class MockConfig extends Mock implements Config {}
+
+class MockProcessManager extends Mock implements ProcessManager {
+  /// We have to send a command that fails in order to get the list of valid
+  /// system images paths. This is an example of the output to use in the mock.
+  static const String mockCreateFailureOutput =
+      'Error: Package path (-k) not specified. Valid system image paths are:\n'
+      'system-images;android-27;google_apis;x86\n'
+      'system-images;android-P;google_apis;x86\n'
+      'system-images;android-27;google_apis_playstore;x86\n'
+      'null\n'; // Yep, these really end with null (on dantup's machine at least)
+
+  static const ListEquality<String> _equality = ListEquality<String>();
+  final List<String> _existingAvds = <String>['existing-avd-1'];
+
+  @override
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String workingDirectory,
+    Map<String, String> environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    final String program = command[0];
+    final List<String> args = command.sublist(1);
+    switch (command[0]) {
+      case '/usr/bin/xcode-select':
+        throw ProcessException(program, args);
+        break;
+      case 'emulator':
+        return _handleEmulator(args);
+      case 'avdmanager':
+        return _handleAvdManager(args);
+    }
+    throw StateError('Unexpected process call: $command');
+  }
+
+  ProcessResult _handleEmulator(List<String> args) {
+    if (_equality.equals(args, <String>['-list-avds'])) {
+      return ProcessResult(101, 0, '${_existingAvds.join('\n')}\n', '');
+    }
+    throw ProcessException('emulator', args);
+  }
+
+  ProcessResult _handleAvdManager(List<String> args) {
+    if (_equality.equals(args, <String>['list', 'device', '-c'])) {
+      return ProcessResult(101, 0, 'test\ntest2\npixel\npixel-xl\n', '');
+    }
+    if (_equality.equals(args, <String>['create', 'avd', '-n', 'temp'])) {
+      return ProcessResult(101, 1, '', mockCreateFailureOutput);
+    }
+    if (args.length == 8 &&
+        _equality.equals(args,
+            <String>['create', 'avd', '-n', args[3], '-k', args[5], '-d', args[7]])) {
+      // In order to support testing auto generation of names we need to support
+      // tracking any created emulators and reject when they already exist so this
+      // mock will compare the name of the AVD being created with the fake existing
+      // list and either reject if it exists, or add it to the list and return success.
+      final String name = args[3];
+      // Error if this AVD already existed
+      if (_existingAvds.contains(name)) {
+        return ProcessResult(
+            101,
+            1,
+            '',
+            "Error: Android Virtual Device '$name' already exists.\n"
+            'Use --force if you want to replace it.');
+      } else {
+        _existingAvds.add(name);
+        return ProcessResult(101, 0, '', '');
+      }
+    }
+    throw ProcessException('emulator', args);
+  }
+}
+
+class MockXcode extends Mock implements Xcode {}
diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart
new file mode 100644
index 0000000..d3bb870
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/flutter_manifest_test.dart
@@ -0,0 +1,560 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+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/logger.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/flutter_manifest.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/pubspec_schema.dart';
+
+void main() {
+  setUpAll(() {
+    Cache.flutterRoot = getFlutterRoot();
+  });
+
+  group('FlutterManifest', () {
+    testUsingContext('is empty when the pubspec.yaml file is empty', () async {
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString('');
+      expect(flutterManifest.isEmpty, true);
+      expect(flutterManifest.appName, '');
+      expect(flutterManifest.usesMaterialDesign, false);
+      expect(flutterManifest.fontsDescriptor, isEmpty);
+      expect(flutterManifest.fonts, isEmpty);
+      expect(flutterManifest.assets, isEmpty);
+    });
+
+    test('has no fonts or assets when the "flutter" section is empty', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest, isNotNull);
+      expect(flutterManifest.isEmpty, false);
+      expect(flutterManifest.appName, 'test');
+      expect(flutterManifest.usesMaterialDesign, false);
+      expect(flutterManifest.fontsDescriptor, isEmpty);
+      expect(flutterManifest.fonts, isEmpty);
+      expect(flutterManifest.assets, isEmpty);
+    });
+
+    test('knows if material design is used', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.usesMaterialDesign, true);
+    });
+
+    test('has two assets', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  assets:
+    - a/foo
+    - a/bar
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.assets.length, 2);
+      expect(flutterManifest.assets[0], Uri.parse('a/foo'));
+      expect(flutterManifest.assets[1], Uri.parse('a/bar'));
+    });
+
+    test('has one font family with one asset', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      final dynamic expectedFontsDescriptor = [{'fonts': [{'asset': 'a/bar'}], 'family': 'foo'}]; // ignore: always_specify_types
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 1);
+      final Font font = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(font.descriptor, fooFontDescriptor);
+      expect(font.familyName, 'foo');
+      final List<FontAsset> assets = font.fontAssets;
+      expect(assets.length, 1);
+      final FontAsset fontAsset = assets[0];
+      expect(fontAsset.assetUri.path, 'a/bar');
+      expect(fontAsset.weight, isNull);
+      expect(fontAsset.style, isNull);
+    });
+
+    test('has one font family with a simple asset and one with weight', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+        - asset: a/bar
+          weight: 400
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      final dynamic expectedFontsDescriptor = [{'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'asset': 'a/bar'}], 'family': 'foo'}]; // ignore: always_specify_types
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 1);
+      final Font font = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(font.descriptor, fooFontDescriptor);
+      expect(font.familyName, 'foo');
+      final List<FontAsset> assets = font.fontAssets;
+      expect(assets.length, 2);
+      final FontAsset fontAsset0 = assets[0];
+      expect(fontAsset0.assetUri.path, 'a/bar');
+      expect(fontAsset0.weight, isNull);
+      expect(fontAsset0.style, isNull);
+      final FontAsset fontAsset1 = assets[1];
+      expect(fontAsset1.assetUri.path, 'a/bar');
+      expect(fontAsset1.weight, 400);
+      expect(fontAsset1.style, isNull);
+    });
+
+    test('has one font family with a simple asset and one with weight and style', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+        - asset: a/bar
+          weight: 400
+          style: italic
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      final dynamic expectedFontsDescriptor = [{'fonts': [{'asset': 'a/bar'}, {'style': 'italic', 'weight': 400, 'asset': 'a/bar'}], 'family': 'foo'}]; // ignore: always_specify_types
+
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 1);
+      final Font font = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'style': 'italic', 'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(font.descriptor, fooFontDescriptor);
+      expect(font.familyName, 'foo');
+      final List<FontAsset> assets = font.fontAssets;
+      expect(assets.length, 2);
+      final FontAsset fontAsset0 = assets[0];
+      expect(fontAsset0.assetUri.path, 'a/bar');
+      expect(fontAsset0.weight, isNull);
+      expect(fontAsset0.style, isNull);
+      final FontAsset fontAsset1 = assets[1];
+      expect(fontAsset1.assetUri.path, 'a/bar');
+      expect(fontAsset1.weight, 400);
+      expect(fontAsset1.style, 'italic');
+    });
+
+    test('has two font families, each with one simple asset and one with weight and style', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+        - asset: a/bar
+          weight: 400
+          style: italic
+    - family: bar
+      fonts:
+        - asset: a/baz
+        - weight: 400
+          asset: a/baz
+          style: italic
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      final dynamic expectedFontsDescriptor = <dynamic>[
+          {'fonts': [{'asset': 'a/bar'}, {'style': 'italic', 'weight': 400, 'asset': 'a/bar'}], 'family': 'foo'}, // ignore: always_specify_types
+          {'fonts': [{'asset': 'a/baz'}, {'style': 'italic', 'weight': 400, 'asset': 'a/baz'}], 'family': 'bar'}, // ignore: always_specify_types
+      ];
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 2);
+
+      final Font fooFont = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'style': 'italic', 'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(fooFont.descriptor, fooFontDescriptor);
+      expect(fooFont.familyName, 'foo');
+      final List<FontAsset> fooAassets = fooFont.fontAssets;
+      expect(fooAassets.length, 2);
+      final FontAsset fooFontAsset0 = fooAassets[0];
+      expect(fooFontAsset0.assetUri.path, 'a/bar');
+      expect(fooFontAsset0.weight, isNull);
+      expect(fooFontAsset0.style, isNull);
+      final FontAsset fooFontAsset1 = fooAassets[1];
+      expect(fooFontAsset1.assetUri.path, 'a/bar');
+      expect(fooFontAsset1.weight, 400);
+      expect(fooFontAsset1.style, 'italic');
+
+      final Font barFont = fonts[1];
+      const String fontDescriptor = '{family: bar, fonts: [{asset: a/baz}, {weight: 400, style: italic, asset: a/baz}]}'; // ignore: always_specify_types
+      expect(barFont.descriptor.toString(), fontDescriptor);
+      expect(barFont.familyName, 'bar');
+      final List<FontAsset> barAssets = barFont.fontAssets;
+      expect(barAssets.length, 2);
+      final FontAsset barFontAsset0 = barAssets[0];
+      expect(barFontAsset0.assetUri.path, 'a/baz');
+      expect(barFontAsset0.weight, isNull);
+      expect(barFontAsset0.style, isNull);
+      final FontAsset barFontAsset1 = barAssets[1];
+      expect(barFontAsset1.assetUri.path, 'a/baz');
+      expect(barFontAsset1.weight, 400);
+      expect(barFontAsset1.style, 'italic');
+    });
+
+    testUsingContext('has only one of two font families when one declaration is missing the "family" option', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+        - asset: a/bar
+          weight: 400
+          style: italic
+    - fonts:
+        - asset: a/baz
+        - asset: a/baz
+          weight: 400
+          style: italic
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+
+      final dynamic expectedFontsDescriptor = [{'fonts': [{'asset': 'a/bar'}, {'style': 'italic', 'weight': 400, 'asset': 'a/bar'}], 'family': 'foo'}]; // ignore: always_specify_types
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 1);
+      final Font fooFont = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'style': 'italic', 'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(fooFont.descriptor, fooFontDescriptor);
+      expect(fooFont.familyName, 'foo');
+      final List<FontAsset> fooAassets = fooFont.fontAssets;
+      expect(fooAassets.length, 2);
+      final FontAsset fooFontAsset0 = fooAassets[0];
+      expect(fooFontAsset0.assetUri.path, 'a/bar');
+      expect(fooFontAsset0.weight, isNull);
+      expect(fooFontAsset0.style, isNull);
+      final FontAsset fooFontAsset1 = fooAassets[1];
+      expect(fooFontAsset1.assetUri.path, 'a/bar');
+      expect(fooFontAsset1.weight, 400);
+      expect(fooFontAsset1.style, 'italic');
+    });
+
+    testUsingContext('has only one of two font families when one declaration is missing the "fonts" option', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - asset: a/bar
+        - asset: a/bar
+          weight: 400
+          style: italic
+    - family: bar
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      final dynamic expectedFontsDescriptor = [{'fonts': [{'asset': 'a/bar'}, {'style': 'italic', 'weight': 400, 'asset': 'a/bar'}], 'family': 'foo'}]; // ignore: always_specify_types
+      expect(flutterManifest.fontsDescriptor, expectedFontsDescriptor);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 1);
+      final Font fooFont = fonts[0];
+      final dynamic fooFontDescriptor = {'family': 'foo', 'fonts': [{'asset': 'a/bar'}, {'weight': 400, 'style': 'italic', 'asset': 'a/bar'}]}; // ignore: always_specify_types
+      expect(fooFont.descriptor, fooFontDescriptor);
+      expect(fooFont.familyName, 'foo');
+      final List<FontAsset> fooAassets = fooFont.fontAssets;
+      expect(fooAassets.length, 2);
+      final FontAsset fooFontAsset0 = fooAassets[0];
+      expect(fooFontAsset0.assetUri.path, 'a/bar');
+      expect(fooFontAsset0.weight, isNull);
+      expect(fooFontAsset0.style, isNull);
+      final FontAsset fooFontAsset1 = fooAassets[1];
+      expect(fooFontAsset1.assetUri.path, 'a/bar');
+      expect(fooFontAsset1.weight, 400);
+      expect(fooFontAsset1.style, 'italic');
+    });
+
+    testUsingContext('has no font family when declaration is missing the "asset" option', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  uses-material-design: true
+  fonts:
+    - family: foo
+      fonts:
+        - weight: 400
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+
+      expect(flutterManifest.fontsDescriptor, <dynamic>[]);
+      final List<Font> fonts = flutterManifest.fonts;
+      expect(fonts.length, 0);
+    });
+
+    test('allows a blank flutter section', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.isEmpty, false);
+      expect(flutterManifest.isModule, false);
+      expect(flutterManifest.isPlugin, false);
+      expect(flutterManifest.androidPackage, null);
+    });
+
+    test('allows a module declaration', () async {
+      const String manifest = '''
+name: test
+flutter:
+  module:
+    androidPackage: com.example
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.isModule, true);
+      expect(flutterManifest.androidPackage, 'com.example');
+    });
+
+    test('allows a plugin declaration', () async {
+      const String manifest = '''
+name: test
+flutter:
+  plugin:
+    androidPackage: com.example
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.isPlugin, true);
+      expect(flutterManifest.androidPackage, 'com.example');
+    });
+
+    Future<void> checkManifestVersion({
+      String manifest,
+      String expectedAppVersion,
+      String expectedBuildName,
+      String expectedBuildNumber,
+    }) async {
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.appVersion, expectedAppVersion);
+      expect(flutterManifest.buildName, expectedBuildName);
+      expect(flutterManifest.buildNumber, expectedBuildNumber);
+    }
+
+    test('parses major.minor.patch+build version clause 1', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+2
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1.0.0+2',
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '2',
+      );
+    });
+
+    test('parses major.minor.patch+build version clause 2', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0-beta+exp.sha.5114f85
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1.0.0-beta+exp.sha.5114f85',
+        expectedBuildName: '1.0.0-beta',
+        expectedBuildNumber: 'exp.sha.5114f85',
+      );
+    });
+
+    test('parses major.minor+build version clause', () async {
+      const String manifest = '''
+name: test
+version: 1.0+2
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1.0+2',
+        expectedBuildName: '1.0',
+        expectedBuildNumber: '2',
+      );
+    });
+
+    test('parses empty version clause', () async {
+      const String manifest = '''
+name: test
+version:
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: null,
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+    });
+
+    test('parses no version clause', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: null,
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+    });
+
+    // Regression test for https://github.com/flutter/flutter/issues/31764
+    testUsingContext('Returns proper error when font detail is malformed', () async {
+      final BufferLogger logger = context.get<Logger>();
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+  fonts:
+    - family: foo
+      fonts:
+        -asset: a/bar
+''';
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+
+      expect(flutterManifest, null);
+      expect(logger.errorText, contains('Expected "fonts" to either be null or a list.'));
+    });
+  });
+
+  group('FlutterManifest with MemoryFileSystem', () {
+    Future<void> assertSchemaIsReadable() async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+
+      final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.isEmpty, false);
+    }
+
+    void testUsingContextAndFs(
+      String description,
+      FileSystem filesystem,
+      dynamic testMethod(),
+    ) {
+      testUsingContext(
+        description,
+        () async {
+          writeEmptySchemaFile(filesystem);
+          testMethod();
+        },
+        overrides: <Type, Generator>{
+          FileSystem: () => filesystem,
+        },
+      );
+    }
+
+    testUsingContext('Validate manifest on original fs', () {
+      assertSchemaIsReadable();
+    });
+
+    testUsingContextAndFs(
+      'Validate manifest on Posix FS',
+      MemoryFileSystem(style: FileSystemStyle.posix),
+      () {
+        assertSchemaIsReadable();
+      },
+    );
+
+    testUsingContextAndFs(
+      'Validate manifest on Windows FS',
+      MemoryFileSystem(style: FileSystemStyle.windows),
+      () {
+        assertSchemaIsReadable();
+      },
+    );
+
+  });
+
+}
+
diff --git a/packages/flutter_tools/test/general.shard/flutter_platform_test.dart b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart
new file mode 100644
index 0000000..2b3c0d7
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/flutter_platform_test.dart
@@ -0,0 +1,167 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/test/flutter_platform.dart';
+import 'package:meta/meta.dart';
+
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+import 'package:test_core/backend.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('FlutterPlatform', () {
+    testUsingContext('ensureConfiguration throws an error if an explicitObservatoryPort is specified and more than one test file', () async {
+      final FlutterPlatform flutterPlatform = FlutterPlatform(shellPath: '/', explicitObservatoryPort: 1234);
+      flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
+      expect(() => flutterPlatform.loadChannel('test2.dart', MockSuitePlatform()), throwsA(isA<ToolExit>()));
+    });
+
+    testUsingContext('ensureConfiguration throws an error if a precompiled entrypoint is specified and more that one test file', () {
+      final FlutterPlatform flutterPlatform = FlutterPlatform(shellPath: '/', precompiledDillPath: 'example.dill');
+      flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
+      expect(() => flutterPlatform.loadChannel('test2.dart', MockSuitePlatform()), throwsA(isA<ToolExit>()));
+    });
+
+    group('The FLUTTER_TEST environment variable is passed to the test process', () {
+      MockPlatform mockPlatform;
+      MockProcessManager mockProcessManager;
+      FlutterPlatform flutterPlatform;
+      final Map<Type, Generator> contextOverrides = <Type, Generator>{
+        Platform: () => mockPlatform,
+        ProcessManager: () => mockProcessManager,
+      };
+
+      setUp(() {
+        mockPlatform = MockPlatform();
+        mockProcessManager = MockProcessManager();
+        flutterPlatform = TestFlutterPlatform();
+      });
+
+      Future<Map<String, String>> captureEnvironment() async {
+        flutterPlatform.loadChannel('test1.dart', MockSuitePlatform());
+        await untilCalled(mockProcessManager.start(any, environment: anyNamed('environment')));
+        final VerificationResult toVerify = verify(mockProcessManager.start(any, environment: captureAnyNamed('environment')));
+        expect(toVerify.captured, hasLength(1));
+        expect(toVerify.captured.first, isInstanceOf<Map<String, String>>());
+        return toVerify.captured.first;
+      }
+
+      testUsingContext('as true when not originally set', () async {
+        when(mockPlatform.environment).thenReturn(<String, String>{});
+        final Map<String, String> capturedEnvironment = await captureEnvironment();
+        expect(capturedEnvironment['FLUTTER_TEST'], 'true');
+      }, overrides: contextOverrides);
+
+      testUsingContext('as true when set to true', () async {
+        when(mockPlatform.environment).thenReturn(<String, String>{'FLUTTER_TEST': 'true'});
+        final Map<String, String> capturedEnvironment = await captureEnvironment();
+        expect(capturedEnvironment['FLUTTER_TEST'], 'true');
+      }, overrides: contextOverrides);
+
+      testUsingContext('as false when set to false', () async {
+        when(mockPlatform.environment).thenReturn(<String, String>{'FLUTTER_TEST': 'false'});
+        final Map<String, String> capturedEnvironment = await captureEnvironment();
+        expect(capturedEnvironment['FLUTTER_TEST'], 'false');
+      }, overrides: contextOverrides);
+
+      testUsingContext('unchanged when set', () async {
+        when(mockPlatform.environment).thenReturn(<String, String>{'FLUTTER_TEST': 'neither true nor false'});
+        final Map<String, String> capturedEnvironment = await captureEnvironment();
+        expect(capturedEnvironment['FLUTTER_TEST'], 'neither true nor false');
+      }, overrides: contextOverrides);
+
+      testUsingContext('as null when set to null', () async {
+        when(mockPlatform.environment).thenReturn(<String, String>{'FLUTTER_TEST': null});
+        final Map<String, String> capturedEnvironment = await captureEnvironment();
+        expect(capturedEnvironment['FLUTTER_TEST'], null);
+      }, overrides: contextOverrides);
+    });
+
+    testUsingContext('installHook creates a FlutterPlatform', () {
+      expect(() => installHook(
+        shellPath: 'abc',
+        enableObservatory: false,
+        startPaused: true
+      ), throwsA(isA<AssertionError>()));
+
+      expect(() => installHook(
+        shellPath: 'abc',
+        enableObservatory: false,
+        startPaused: false,
+        observatoryPort: 123
+      ), throwsA(isA<AssertionError>()));
+
+      FlutterPlatform capturedPlatform;
+      final Map<String, String> expectedPrecompiledDillFiles = <String, String>{'Key': 'Value'};
+      final FlutterPlatform flutterPlatform = installHook(
+        shellPath: 'abc',
+        enableObservatory: true,
+        machine: true,
+        startPaused: true,
+        disableServiceAuthCodes: true,
+        port: 100,
+        precompiledDillPath: 'def',
+        precompiledDillFiles: expectedPrecompiledDillFiles,
+        trackWidgetCreation: true,
+        updateGoldens: true,
+        buildTestAssets: true,
+        observatoryPort: 200,
+        serverType: InternetAddressType.IPv6,
+        icudtlPath: 'ghi',
+        platformPluginRegistration: (FlutterPlatform platform) {
+          capturedPlatform = platform;
+        });
+
+      expect(identical(capturedPlatform, flutterPlatform), equals(true));
+      expect(flutterPlatform.shellPath, equals('abc'));
+      expect(flutterPlatform.enableObservatory, equals(true));
+      expect(flutterPlatform.machine, equals(true));
+      expect(flutterPlatform.startPaused, equals(true));
+      expect(flutterPlatform.disableServiceAuthCodes, equals(true));
+      expect(flutterPlatform.port, equals(100));
+      expect(flutterPlatform.host, InternetAddress.loopbackIPv6);
+      expect(flutterPlatform.explicitObservatoryPort, equals(200));
+      expect(flutterPlatform.precompiledDillPath, equals('def'));
+      expect(flutterPlatform.precompiledDillFiles, expectedPrecompiledDillFiles);
+      expect(flutterPlatform.trackWidgetCreation, equals(true));
+      expect(flutterPlatform.updateGoldens, equals(true));
+      expect(flutterPlatform.buildTestAssets, equals(true));
+      expect(flutterPlatform.icudtlPath, equals('ghi'));
+    });
+  });
+}
+
+class MockSuitePlatform extends Mock implements SuitePlatform {}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockPlatform extends Mock implements Platform {}
+
+class MockHttpServer extends Mock implements HttpServer {}
+
+// A FlutterPlatform with enough fields set to load and start a test.
+//
+// Uses a mock HttpServer. We don't want to bind random ports in our CI hosts.
+class TestFlutterPlatform extends FlutterPlatform {
+  TestFlutterPlatform() : super(
+    shellPath: '/',
+    precompiledDillPath: 'example.dill',
+    host: InternetAddress.loopbackIPv6,
+    port: 0,
+    updateGoldens: false,
+    startPaused: false,
+    enableObservatory: false,
+    buildTestAssets: false,
+  );
+
+  @override
+  @protected
+  Future<HttpServer> bind(InternetAddress host, int port) async => MockHttpServer();
+}
diff --git a/packages/flutter_tools/test/general.shard/forbidden_imports_test.dart b/packages/flutter_tools/test/general.shard/forbidden_imports_test.dart
new file mode 100644
index 0000000..1c0e4c5
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/forbidden_imports_test.dart
@@ -0,0 +1,111 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import '../src/common.dart';
+
+void main() {
+  final String flutterTools = fs.path.join(getFlutterRoot(), 'packages', 'flutter_tools');
+
+  test('no unauthorized imports of dart:io', () {
+    final List<String> whitelistedPaths = <String>[
+      fs.path.join(flutterTools, 'lib', 'src', 'base', 'io.dart'),
+    ];
+    bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
+
+    for (String dirName in <String>['lib', 'bin']) {
+      final Iterable<File> files = fs.directory(fs.path.join(flutterTools, dirName))
+        .listSync(recursive: true)
+        .where(_isDartFile)
+        .where(_isNotWhitelisted)
+        .map(_asFile);
+      for (File file in files) {
+        for (String line in file.readAsLinesSync()) {
+          if (line.startsWith(RegExp(r'import.*dart:io')) &&
+              !line.contains('ignore: dart_io_import')) {
+            final String relativePath = fs.path.relative(file.path, from:flutterTools);
+            fail("$relativePath imports 'dart:io'; import 'lib/src/base/io.dart' instead");
+          }
+        }
+      }
+    }
+  });
+
+  test('no unauthorized imports of package:path', () {
+    final String whitelistedPath = fs.path.join(flutterTools, 'lib', 'src', 'build_runner', 'web_compilation_delegate.dart');
+    for (String dirName in <String>['lib', 'bin', 'test']) {
+      final Iterable<File> files = fs.directory(fs.path.join(flutterTools, dirName))
+        .listSync(recursive: true)
+        .where(_isDartFile)
+        .where((FileSystemEntity entity) => entity.path != whitelistedPath)
+        .map(_asFile);
+      for (File file in files) {
+        for (String line in file.readAsLinesSync()) {
+          if (line.startsWith(RegExp(r'import.*package:path/path.dart')) &&
+              !line.contains('ignore: package_path_import')) {
+            final String relativePath = fs.path.relative(file.path, from:flutterTools);
+            fail("$relativePath imports 'package:path/path.dart'; use 'fs.path' instead");
+          }
+        }
+      }
+    }
+  });
+
+  test('no unauthorized imports of dart:convert', () {
+    final List<String> whitelistedPaths = <String>[
+      fs.path.join(flutterTools, 'lib', 'src', 'convert.dart'),
+    ];
+    bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
+
+    for (String dirName in <String>['lib']) {
+      final Iterable<File> files = fs.directory(fs.path.join(flutterTools, dirName))
+        .listSync(recursive: true)
+        .where(_isDartFile)
+        .where(_isNotWhitelisted)
+        .map(_asFile);
+      for (File file in files) {
+        for (String line in file.readAsLinesSync()) {
+          if (line.startsWith(RegExp(r'import.*dart:convert')) &&
+              !line.contains('ignore: dart_convert_import')) {
+            final String relativePath = fs.path.relative(file.path, from:flutterTools);
+            fail("$relativePath imports 'dart:convert'; import 'lib/src/convert.dart' instead");
+          }
+        }
+      }
+    }
+  });
+
+  test('no unauthorized imports of build_runner', () {
+    final List<String> whitelistedPaths = <String>[
+      fs.path.join(flutterTools, 'test', 'src', 'build_runner'),
+      fs.path.join(flutterTools, 'lib', 'src', 'build_runner'),
+      fs.path.join(flutterTools, 'lib', 'executable.dart'),
+    ];
+    bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => !entity.path.contains(path));
+
+    for (String dirName in <String>['lib']) {
+      final Iterable<File> files = fs.directory(fs.path.join(flutterTools, dirName))
+        .listSync(recursive: true)
+        .where(_isDartFile)
+        .where(_isNotWhitelisted)
+        .map(_asFile);
+      for (File file in files) {
+        for (String line in file.readAsLinesSync()) {
+          if (line.startsWith(RegExp(r'import.*package:build_runner_core/build_runner_core.dart')) ||
+              line.startsWith(RegExp(r'import.*package:build_runner/build_runner.dart')) ||
+              line.startsWith(RegExp(r'import.*package:build_config/build_config.dart')) ||
+              line.startsWith(RegExp(r'import.*build_runner/.*.dart'))) {
+            final String relativePath = fs.path.relative(file.path, from:flutterTools);
+            fail('$relativePath imports a build_runner package');
+          }
+        }
+      }
+    }
+  });
+}
+
+bool _isDartFile(FileSystemEntity entity) => entity is File && entity.path.endsWith('.dart');
+
+File _asFile(FileSystemEntity entity) => entity;
diff --git a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
new file mode 100644
index 0000000..e63ec84
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
@@ -0,0 +1,1077 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/common.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/os.dart';
+import 'package:flutter_tools/src/base/time.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/fuchsia/application_package.dart';
+import 'package:flutter_tools/src/fuchsia/amber_ctl.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_dev_finder.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_kernel_compiler.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_pm.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
+import 'package:flutter_tools/src/fuchsia/tiles_ctl.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('fuchsia device', () {
+    MemoryFileSystem memoryFileSystem;
+    setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+    });
+
+    testUsingContext('stores the requested id and name', () {
+      const String deviceId = 'e80::0000:a00a:f00f:2002/3';
+      const String name = 'halfbaked';
+      final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);
+      expect(device.id, deviceId);
+      expect(device.name, name);
+    });
+
+    test('parse dev_finder output', () {
+      const String example = '192.168.42.56 paper-pulp-bush-angel';
+      final List<FuchsiaDevice> names = parseListDevices(example);
+
+      expect(names.length, 1);
+      expect(names.first.name, 'paper-pulp-bush-angel');
+      expect(names.first.id, '192.168.42.56');
+    });
+
+    test('parse junk dev_finder output', () {
+      const String example = 'junk';
+      final List<FuchsiaDevice> names = parseListDevices(example);
+
+      expect(names.length, 0);
+    });
+
+    testUsingContext('default capabilities', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      fs.directory('fuchsia').createSync(recursive: true);
+      fs.file('pubspec.yaml').createSync();
+
+      expect(device.supportsHotReload, true);
+      expect(device.supportsHotRestart, false);
+      expect(device.supportsFlutterExit, false);
+      expect(device.isSupportedForProject(FlutterProject.current()), true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+    });
+
+    testUsingContext('supported for project', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      fs.directory('fuchsia').createSync(recursive: true);
+      fs.file('pubspec.yaml').createSync();
+      expect(device.isSupportedForProject(FlutterProject.current()), true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+    });
+
+    testUsingContext('not supported for project', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      fs.file('pubspec.yaml').createSync();
+      expect(device.isSupportedForProject(FlutterProject.current()), false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+    });
+  });
+
+  group('Fuchsia device artifact overrides', () {
+    MockFile devFinder;
+    MockFile sshConfig;
+    MockFile platformDill;
+    MockFile patchedSdk;
+
+    setUp(() {
+      devFinder = MockFile();
+      sshConfig = MockFile();
+      platformDill = MockFile();
+      patchedSdk = MockFile();
+      when(devFinder.absolute).thenReturn(devFinder);
+      when(sshConfig.absolute).thenReturn(sshConfig);
+      when(platformDill.absolute).thenReturn(platformDill);
+      when(patchedSdk.absolute).thenReturn(patchedSdk);
+    });
+
+    testUsingContext('exist', () async {
+      final FuchsiaDevice device = FuchsiaDevice('fuchsia-device');
+      expect(device.artifactOverrides, isNotNull);
+      expect(device.artifactOverrides.platformKernelDill, equals(platformDill));
+      expect(device.artifactOverrides.flutterPatchedSdk, equals(patchedSdk));
+    }, overrides: <Type, Generator>{
+      FuchsiaArtifacts: () => FuchsiaArtifacts(
+            sshConfig: sshConfig,
+            devFinder: devFinder,
+            platformKernelDill: platformDill,
+            flutterPatchedSdk: patchedSdk,
+          ),
+    });
+
+    testUsingContext('are used', () async {
+      final FuchsiaDevice device = FuchsiaDevice('fuchsia-device');
+      expect(device.artifactOverrides, isNotNull);
+      expect(device.artifactOverrides.platformKernelDill, equals(platformDill));
+      expect(device.artifactOverrides.flutterPatchedSdk, equals(patchedSdk));
+      await context.run<void>(
+        body: () {
+          expect(Artifacts.instance.getArtifactPath(Artifact.platformKernelDill),
+                 equals(platformDill.path));
+          expect(Artifacts.instance.getArtifactPath(Artifact.flutterPatchedSdkPath),
+                 equals(patchedSdk.path));
+        },
+        overrides: <Type, Generator>{
+          Artifacts: () => device.artifactOverrides,
+        },
+      );
+    }, overrides: <Type, Generator>{
+      FuchsiaArtifacts: () => FuchsiaArtifacts(
+            sshConfig: sshConfig,
+            devFinder: devFinder,
+            platformKernelDill: platformDill,
+            flutterPatchedSdk: patchedSdk,
+          ),
+    });
+  });
+
+  group('displays friendly error when', () {
+    MockProcessManager mockProcessManager;
+    MockProcessResult mockProcessResult;
+    MockFile mockFile;
+    MockProcessManager emptyStdoutProcessManager;
+    MockProcessResult emptyStdoutProcessResult;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockProcessResult = MockProcessResult();
+      mockFile = MockFile();
+      when(mockProcessManager.run(
+        any,
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) =>
+          Future<ProcessResult>.value(mockProcessResult));
+      when(mockProcessResult.exitCode).thenReturn(1);
+      when<String>(mockProcessResult.stdout).thenReturn('');
+      when<String>(mockProcessResult.stderr).thenReturn('');
+      when(mockFile.absolute).thenReturn(mockFile);
+      when(mockFile.path).thenReturn('');
+
+      emptyStdoutProcessManager = MockProcessManager();
+      emptyStdoutProcessResult = MockProcessResult();
+      when(emptyStdoutProcessManager.run(
+        any,
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) =>
+          Future<ProcessResult>.value(emptyStdoutProcessResult));
+      when(emptyStdoutProcessResult.exitCode).thenReturn(0);
+      when<String>(emptyStdoutProcessResult.stdout).thenReturn('');
+      when<String>(emptyStdoutProcessResult.stderr).thenReturn('');
+    });
+
+    testUsingContext('No vmservices found', () async {
+      final FuchsiaDevice device = FuchsiaDevice('id');
+      ToolExit toolExit;
+      try {
+        await device.servicePorts();
+      } on ToolExit catch (err) {
+        toolExit = err;
+      }
+      expect(
+          toolExit.message,
+          contains(
+              'No Dart Observatories found. Are you running a debug build?'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => emptyStdoutProcessManager,
+      FuchsiaArtifacts: () => FuchsiaArtifacts(
+            sshConfig: mockFile,
+            devFinder: mockFile,
+          ),
+    });
+
+    group('device logs', () {
+      const String exampleUtcLogs = '''
+[2018-11-09 01:27:45][3][297950920][log] INFO: example_app.cmx(flutter): Error doing thing
+[2018-11-09 01:27:58][46257][46269][foo] INFO: Using a thing
+[2018-11-09 01:29:58][46257][46269][foo] INFO: Blah blah blah
+[2018-11-09 01:29:58][46257][46269][foo] INFO: other_app.cmx(flutter): Do thing
+[2018-11-09 01:30:02][41175][41187][bar] INFO: Invoking a bar
+[2018-11-09 01:30:12][52580][52983][log] INFO: example_app.cmx(flutter): Did thing this time
+
+  ''';
+      MockProcessManager mockProcessManager;
+      MockProcess mockProcess;
+      Completer<int> exitCode;
+      StreamController<List<int>> stdout;
+      StreamController<List<int>> stderr;
+      MockFile devFinder;
+      MockFile sshConfig;
+
+      setUp(() {
+        mockProcessManager = MockProcessManager();
+        mockProcess = MockProcess();
+        stdout = StreamController<List<int>>(sync: true);
+        stderr = StreamController<List<int>>(sync: true);
+        exitCode = Completer<int>();
+        when(mockProcessManager.start(any))
+            .thenAnswer((Invocation _) => Future<Process>.value(mockProcess));
+        when(mockProcess.exitCode).thenAnswer((Invocation _) => exitCode.future);
+        when(mockProcess.stdout).thenAnswer((Invocation _) => stdout.stream);
+        when(mockProcess.stderr).thenAnswer((Invocation _) => stderr.stream);
+        devFinder = MockFile();
+        sshConfig = MockFile();
+        when(devFinder.existsSync()).thenReturn(true);
+        when(sshConfig.existsSync()).thenReturn(true);
+        when(devFinder.absolute).thenReturn(devFinder);
+        when(sshConfig.absolute).thenReturn(sshConfig);
+      });
+
+      tearDown(() {
+        exitCode.complete(0);
+      });
+
+      testUsingContext('can be parsed for an app', () async {
+        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+        final DeviceLogReader reader = device.getLogReader(
+            app: FuchsiaModulePackage(name: 'example_app'));
+        final List<String> logLines = <String>[];
+        final Completer<void> lock = Completer<void>();
+        reader.logLines.listen((String line) {
+          logLines.add(line);
+          if (logLines.length == 2) {
+            lock.complete();
+          }
+        });
+        expect(logLines, isEmpty);
+
+        stdout.add(utf8.encode(exampleUtcLogs));
+        await stdout.close();
+        await lock.future.timeout(const Duration(seconds: 1));
+
+        expect(logLines, <String>[
+          '[2018-11-09 01:27:45.000] Flutter: Error doing thing',
+          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
+        ]);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
+        FuchsiaArtifacts: () =>
+            FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
+      });
+
+      testUsingContext('cuts off prior logs', () async {
+        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+        final DeviceLogReader reader = device.getLogReader(
+            app: FuchsiaModulePackage(name: 'example_app'));
+        final List<String> logLines = <String>[];
+        final Completer<void> lock = Completer<void>();
+        reader.logLines.listen((String line) {
+          logLines.add(line);
+          lock.complete();
+        });
+        expect(logLines, isEmpty);
+
+        stdout.add(utf8.encode(exampleUtcLogs));
+        await stdout.close();
+        await lock.future.timeout(const Duration(seconds: 1));
+
+        expect(logLines, <String>[
+          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
+        ]);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 29, 45)),
+        FuchsiaArtifacts: () =>
+            FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
+      });
+
+      testUsingContext('can be parsed for all apps', () async {
+        final FuchsiaDevice device = FuchsiaDevice('id', name: 'tester');
+        final DeviceLogReader reader = device.getLogReader();
+        final List<String> logLines = <String>[];
+        final Completer<void> lock = Completer<void>();
+        reader.logLines.listen((String line) {
+          logLines.add(line);
+          if (logLines.length == 3) {
+            lock.complete();
+          }
+        });
+        expect(logLines, isEmpty);
+
+        stdout.add(utf8.encode(exampleUtcLogs));
+        await stdout.close();
+        await lock.future.timeout(const Duration(seconds: 1));
+
+        expect(logLines, <String>[
+          '[2018-11-09 01:27:45.000] Flutter: Error doing thing',
+          '[2018-11-09 01:29:58.000] Flutter: Do thing',
+          '[2018-11-09 01:30:12.000] Flutter: Did thing this time',
+        ]);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        SystemClock: () => SystemClock.fixed(DateTime(2018, 11, 9, 1, 25, 45)),
+        FuchsiaArtifacts: () =>
+            FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
+      });
+    });
+  });
+
+  group(FuchsiaIsolateDiscoveryProtocol, () {
+    Future<Uri> findUri(
+        List<MockFlutterView> views, String expectedIsolateName) {
+      final MockPortForwarder portForwarder = MockPortForwarder();
+      final MockVMService vmService = MockVMService();
+      final MockVM vm = MockVM();
+      vm.vmService = vmService;
+      vmService.vm = vm;
+      vm.views = views;
+      for (MockFlutterView view in views) {
+        view.owner = vm;
+      }
+      final MockFuchsiaDevice fuchsiaDevice =
+          MockFuchsiaDevice('123', portForwarder, false);
+      final FuchsiaIsolateDiscoveryProtocol discoveryProtocol =
+          FuchsiaIsolateDiscoveryProtocol(
+        fuchsiaDevice,
+        expectedIsolateName,
+        (Uri uri) async => vmService,
+        true, // only poll once.
+      );
+      when(fuchsiaDevice.servicePorts())
+          .thenAnswer((Invocation invocation) async => <int>[1]);
+      when(portForwarder.forward(1))
+          .thenAnswer((Invocation invocation) async => 2);
+      when(vmService.getVM())
+          .thenAnswer((Invocation invocation) => Future<void>.value(null));
+      when(vmService.refreshViews())
+          .thenAnswer((Invocation invocation) => Future<void>.value(null));
+      when(vmService.httpAddress).thenReturn(Uri.parse('example'));
+      return discoveryProtocol.uri;
+    }
+
+    testUsingContext('can find flutter view with matching isolate name',
+        () async {
+      const String expectedIsolateName = 'foobar';
+      final Uri uri = await findUri(<MockFlutterView>[
+        MockFlutterView(null), // no ui isolate.
+        MockFlutterView(MockIsolate('wrong name')), // wrong name.
+        MockFlutterView(MockIsolate(expectedIsolateName)), // matching name.
+      ], expectedIsolateName);
+      expect(
+          uri.toString(), 'http://${InternetAddress.loopbackIPv4.address}:0/');
+    });
+
+    testUsingContext('can handle flutter view without matching isolate name',
+        () async {
+      const String expectedIsolateName = 'foobar';
+      final Future<Uri> uri = findUri(<MockFlutterView>[
+        MockFlutterView(null), // no ui isolate.
+        MockFlutterView(MockIsolate('wrong name')), // wrong name.
+      ], expectedIsolateName);
+      expect(uri, throwsException);
+    });
+
+    testUsingContext('can handle non flutter view', () async {
+      const String expectedIsolateName = 'foobar';
+      final Future<Uri> uri = findUri(<MockFlutterView>[
+        MockFlutterView(null), // no ui isolate.
+      ], expectedIsolateName);
+      expect(uri, throwsException);
+    });
+  });
+
+  group('fuchsia app start and stop: ', () {
+    MemoryFileSystem memoryFileSystem;
+    MockOperatingSystemUtils osUtils;
+    FakeFuchsiaDeviceTools fuchsiaDeviceTools;
+    MockFuchsiaSdk fuchsiaSdk;
+    setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+      osUtils = MockOperatingSystemUtils();
+      fuchsiaDeviceTools = FakeFuchsiaDeviceTools();
+      fuchsiaSdk = MockFuchsiaSdk();
+
+      when(osUtils.findFreePort()).thenAnswer((_) => Future<int>.value(12345));
+    });
+
+    Future<LaunchResult> setupAndStartApp({
+      @required bool prebuilt,
+      @required BuildMode mode,
+    }) async {
+      const String appName = 'app_name';
+      final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123');
+      fs.directory('fuchsia').createSync(recursive: true);
+      final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+      pubspecFile.writeAsStringSync('name: $appName');
+
+      FuchsiaApp app;
+      if (prebuilt) {
+        final File far = fs.file('app_name-0.far')..createSync();
+        app = FuchsiaApp.fromPrebuiltApp(far);
+      } else {
+        fs.file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+          ..createSync(recursive: true)
+          ..writeAsStringSync('{}');
+        fs.file('.packages').createSync();
+        fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+        app = BuildableFuchsiaApp(project: FlutterProject.current().fuchsia);
+      }
+
+      final DebuggingOptions debuggingOptions =
+          DebuggingOptions.disabled(BuildInfo(mode, null));
+      return await device.startApp(app,
+          prebuiltApplication: prebuilt,
+          debuggingOptions: debuggingOptions);
+    }
+
+    testUsingContext('start prebuilt in release mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start and stop prebuilt in release mode', () async {
+      const String appName = 'app_name';
+      final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123');
+      fs.directory('fuchsia').createSync(recursive: true);
+      final File pubspecFile = fs.file('pubspec.yaml')..createSync();
+      pubspecFile.writeAsStringSync('name: $appName');
+      final File far = fs.file('app_name-0.far')..createSync();
+
+      final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far);
+      final DebuggingOptions debuggingOptions =
+          DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null));
+      final LaunchResult launchResult = await device.startApp(app,
+          prebuiltApplication: true,
+          debuggingOptions: debuggingOptions);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isFalse);
+      expect(await device.stopApp(app), isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start prebuilt in debug mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.debug);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start buildable in release mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: false, mode: BuildMode.release);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start buildable in debug mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: false, mode: BuildMode.debug);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when dev_finder fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => MockFuchsiaSdk(devFinder: FailingDevFinder()),
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when pm fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => MockFuchsiaSdk(pm: FailingPM()),
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when amber fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => FakeFuchsiaDeviceTools(amber: FailingAmberCtl()),
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when tiles fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => FakeFuchsiaDeviceTools(tiles: FailingTilesCtl()),
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+  });
+
+  group('sdkNameAndVersion: ', () {
+    MockFile sshConfig;
+    MockProcessManager mockSuccessProcessManager;
+    MockProcessResult mockSuccessProcessResult;
+    MockProcessManager mockFailureProcessManager;
+    MockProcessResult mockFailureProcessResult;
+    MockProcessManager emptyStdoutProcessManager;
+    MockProcessResult emptyStdoutProcessResult;
+
+    setUp(() {
+      sshConfig = MockFile();
+      when(sshConfig.absolute).thenReturn(sshConfig);
+
+      mockSuccessProcessManager = MockProcessManager();
+      mockSuccessProcessResult = MockProcessResult();
+      when(mockSuccessProcessManager.run(any)).thenAnswer(
+          (Invocation invocation) => Future<ProcessResult>.value(mockSuccessProcessResult));
+      when(mockSuccessProcessResult.exitCode).thenReturn(0);
+      when<String>(mockSuccessProcessResult.stdout).thenReturn('version');
+      when<String>(mockSuccessProcessResult.stderr).thenReturn('');
+
+      mockFailureProcessManager = MockProcessManager();
+      mockFailureProcessResult = MockProcessResult();
+      when(mockFailureProcessManager.run(any)).thenAnswer(
+          (Invocation invocation) => Future<ProcessResult>.value(mockFailureProcessResult));
+      when(mockFailureProcessResult.exitCode).thenReturn(1);
+      when<String>(mockFailureProcessResult.stdout).thenReturn('');
+      when<String>(mockFailureProcessResult.stderr).thenReturn('');
+
+      emptyStdoutProcessManager = MockProcessManager();
+      emptyStdoutProcessResult = MockProcessResult();
+      when(emptyStdoutProcessManager.run(any)).thenAnswer((Invocation invocation) =>
+          Future<ProcessResult>.value(emptyStdoutProcessResult));
+      when(emptyStdoutProcessResult.exitCode).thenReturn(0);
+      when<String>(emptyStdoutProcessResult.stdout).thenReturn('');
+      when<String>(emptyStdoutProcessResult.stderr).thenReturn('');
+    });
+
+    testUsingContext('returns what we get from the device on success', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      expect(await device.sdkNameAndVersion, equals('Fuchsia version'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockSuccessProcessManager,
+      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
+    });
+
+    testUsingContext('returns "Fuchsia" when device command fails', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockFailureProcessManager,
+      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
+    });
+
+    testUsingContext('returns "Fuchsia" when device gives an empty result', () async {
+      final FuchsiaDevice device = FuchsiaDevice('123');
+      expect(await device.sdkNameAndVersion, equals('Fuchsia'));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => emptyStdoutProcessManager,
+      FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: sshConfig),
+    });
+  });
+}
+
+class FuchsiaModulePackage extends ApplicationPackage {
+  FuchsiaModulePackage({@required this.name}) : super(id: name);
+
+  @override
+  final String name;
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockProcessResult extends Mock implements ProcessResult {}
+
+class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
+
+class MockFile extends Mock implements File {}
+
+class MockProcess extends Mock implements Process {}
+
+Process _createMockProcess({
+    int exitCode = 0,
+    String stdout = '',
+    String stderr = '',
+    bool persistent = false,
+  }) {
+  final Stream<List<int>> stdoutStream = Stream<List<int>>.fromIterable(<List<int>>[
+    utf8.encode(stdout),
+  ]);
+  final Stream<List<int>> stderrStream = Stream<List<int>>.fromIterable(<List<int>>[
+    utf8.encode(stderr),
+  ]);
+  final Process process = MockProcess();
+
+  when(process.stdout).thenAnswer((_) => stdoutStream);
+  when(process.stderr).thenAnswer((_) => stderrStream);
+
+  if (persistent) {
+    final Completer<int> exitCodeCompleter = Completer<int>();
+    when(process.kill()).thenAnswer((_) {
+      exitCodeCompleter.complete(-11);
+      return true;
+    });
+    when(process.exitCode).thenAnswer((_) => exitCodeCompleter.future);
+  } else {
+    when(process.exitCode).thenAnswer((_) => Future<int>.value(exitCode));
+  }
+  return process;
+}
+
+class MockFuchsiaDevice extends Mock implements FuchsiaDevice {
+  MockFuchsiaDevice(this.id, this.portForwarder, this.ipv6);
+
+  @override
+  final bool ipv6;
+  @override
+  final String id;
+  @override
+  final DevicePortForwarder portForwarder;
+}
+
+class MockPortForwarder extends Mock implements DevicePortForwarder {}
+
+class MockVMService extends Mock implements VMService {
+  @override
+  VM vm;
+}
+
+class MockVM extends Mock implements VM {
+  @override
+  VMService vmService;
+
+  @override
+  List<FlutterView> views;
+}
+
+class MockFlutterView extends Mock implements FlutterView {
+  MockFlutterView(this.uiIsolate);
+
+  @override
+  final Isolate uiIsolate;
+
+  @override
+  ServiceObjectOwner owner;
+}
+
+class MockIsolate extends Mock implements Isolate {
+  MockIsolate(this.name);
+
+  @override
+  final String name;
+}
+
+class FuchsiaDeviceWithFakeDiscovery extends FuchsiaDevice {
+  FuchsiaDeviceWithFakeDiscovery(String id, {String name}) : super(id, name: name);
+
+  @override
+  FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(
+        String isolateName) =>
+    FakeFuchsiaIsolateDiscoveryProtocol();
+}
+
+class FakeFuchsiaIsolateDiscoveryProtocol implements FuchsiaIsolateDiscoveryProtocol {
+  @override
+  FutureOr<Uri> get uri => Uri.parse('http://[::1]:37');
+
+  @override
+  void dispose() {}
+}
+
+class FakeFuchsiaAmberCtl implements FuchsiaAmberCtl {
+  @override
+  Future<bool> addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return true;
+  }
+
+  @override
+  Future<bool> rmSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return true;
+  }
+
+  @override
+  Future<bool> getUp(FuchsiaDevice device, String packageName) async {
+    return true;
+  }
+
+  @override
+  Future<bool> addRepoCfg(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return true;
+  }
+
+  @override
+  Future<bool> pkgCtlResolve(FuchsiaDevice device, FuchsiaPackageServer server,
+                             String packageName) async {
+    return true;
+  }
+
+  @override
+  Future<bool> pkgCtlRepoRemove(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return true;
+  }
+}
+
+class FailingAmberCtl implements FuchsiaAmberCtl {
+  @override
+  Future<bool> addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+
+  @override
+  Future<bool> rmSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+
+  @override
+  Future<bool> getUp(FuchsiaDevice device, String packageName) async {
+    return false;
+  }
+
+  @override
+  Future<bool> addRepoCfg(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+
+  @override
+  Future<bool> pkgCtlResolve(FuchsiaDevice device, FuchsiaPackageServer server,
+                             String packageName) async {
+    return false;
+  }
+
+  @override
+  Future<bool> pkgCtlRepoRemove(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+}
+
+class FakeFuchsiaTilesCtl implements FuchsiaTilesCtl {
+  final Map<int, String> _runningApps = <int, String>{};
+  bool _started = false;
+  int _nextAppId = 1;
+
+  @override
+  Future<bool> start(FuchsiaDevice device) async {
+    _started = true;
+    return true;
+  }
+
+  @override
+  Future<Map<int, String>> list(FuchsiaDevice device) async {
+    if (!_started) {
+      return null;
+    }
+    return _runningApps;
+  }
+
+  @override
+  Future<bool> add(FuchsiaDevice device, String url, List<String> args) async {
+    if (!_started) {
+      return false;
+    }
+    _runningApps[_nextAppId] = url;
+    _nextAppId++;
+    return true;
+  }
+
+  @override
+  Future<bool> remove(FuchsiaDevice device, int key) async {
+    if (!_started) {
+      return false;
+    }
+    _runningApps.remove(key);
+    return true;
+  }
+
+  @override
+  Future<bool> quit(FuchsiaDevice device) async {
+    if (!_started) {
+      return false;
+    }
+    _started = false;
+    return true;
+  }
+}
+
+class FailingTilesCtl implements FuchsiaTilesCtl {
+  @override
+  Future<bool> start(FuchsiaDevice device) async {
+    return false;
+  }
+
+  @override
+  Future<Map<int, String>> list(FuchsiaDevice device) async {
+    return null;
+  }
+
+  @override
+  Future<bool> add(FuchsiaDevice device, String url, List<String> args) async {
+    return false;
+  }
+
+  @override
+  Future<bool> remove(FuchsiaDevice device, int key) async {
+    return false;
+  }
+
+  @override
+  Future<bool> quit(FuchsiaDevice device) async {
+    return false;
+  }
+}
+
+class FakeFuchsiaDeviceTools implements FuchsiaDeviceTools {
+  FakeFuchsiaDeviceTools({
+    FuchsiaAmberCtl amber,
+    FuchsiaTilesCtl tiles,
+  }) : amberCtl = amber ?? FakeFuchsiaAmberCtl(),
+       tilesCtl = tiles ?? FakeFuchsiaTilesCtl();
+
+  @override
+  final FuchsiaAmberCtl amberCtl;
+
+  @override
+  final FuchsiaTilesCtl tilesCtl;
+}
+
+class FakeFuchsiaPM implements FuchsiaPM {
+  String _appName;
+
+  @override
+  Future<bool> init(String buildPath, String appName) async {
+    if (!fs.directory(buildPath).existsSync()) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, 'meta', 'package'))
+        .createSync(recursive: true);
+    _appName = appName;
+    return true;
+  }
+
+  @override
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync()) {
+      return false;
+    }
+    fs.file(outKeyPath).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    fs.file(fs.path.join(buildPath, 'meta.far')).createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    if (!fs.file(fs.path.join(buildPath, 'meta', 'package')).existsSync() ||
+        !fs.file(keyPath).existsSync() ||
+        !fs.file(manifestPath).existsSync()) {
+      return false;
+    }
+    if (_appName == null) {
+      return false;
+    }
+    fs
+        .file(fs.path.join(buildPath, '$_appName-0.far'))
+        .createSync(recursive: true);
+    return true;
+  }
+
+  @override
+  Future<bool> newrepo(String repoPath) async {
+    if (!fs.directory(repoPath).existsSync()) {
+      return false;
+    }
+    return true;
+  }
+
+  @override
+  Future<Process> serve(String repoPath, String host, int port) async {
+    return _createMockProcess(persistent: true);
+  }
+
+  @override
+  Future<bool> publish(String repoPath, String packagePath) async {
+    if (!fs.directory(repoPath).existsSync()) {
+      return false;
+    }
+    if (!fs.file(packagePath).existsSync()) {
+      return false;
+    }
+    return true;
+  }
+}
+
+class FailingPM implements FuchsiaPM {
+  @override
+  Future<bool> init(String buildPath, String appName) async {
+    return false;
+  }
+
+  @override
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> newrepo(String repoPath) async {
+    return false;
+  }
+
+  @override
+  Future<Process> serve(String repoPath, String host, int port) async {
+    return _createMockProcess(exitCode: 6);
+  }
+
+  @override
+  Future<bool> publish(String repoPath, String packagePath) async {
+    return false;
+  }
+}
+
+class FakeFuchsiaKernelCompiler implements FuchsiaKernelCompiler {
+  @override
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    final String outDir = getFuchsiaBuildDirectory();
+    final String appName = fuchsiaProject.project.manifest.appName;
+    final String manifestPath = fs.path.join(outDir, '$appName.dilpmanifest');
+    fs.file(manifestPath).createSync(recursive: true);
+  }
+}
+
+class FailingKernelCompiler implements FuchsiaKernelCompiler {
+  @override
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    throwToolExit('Build process failed');
+  }
+}
+
+class FakeFuchsiaDevFinder implements FuchsiaDevFinder {
+  @override
+  Future<List<String>> list() async {
+    return <String>['192.168.42.172 scare-cable-skip-joy'];
+  }
+
+  @override
+  Future<String> resolve(String deviceName) async {
+    return '192.168.42.10';
+  }
+}
+
+class FailingDevFinder implements FuchsiaDevFinder {
+  @override
+  Future<List<String>> list() async {
+    return null;
+  }
+
+  @override
+  Future<String> resolve(String deviceName) async {
+    return null;
+  }
+}
+
+class MockFuchsiaSdk extends Mock implements FuchsiaSdk {
+  MockFuchsiaSdk({
+    FuchsiaPM pm,
+    FuchsiaKernelCompiler compiler,
+    FuchsiaDevFinder devFinder,
+  }) : fuchsiaPM = pm ?? FakeFuchsiaPM(),
+       fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(),
+       fuchsiaDevFinder = devFinder ?? FakeFuchsiaDevFinder();
+
+  @override
+  final FuchsiaPM fuchsiaPM;
+
+  @override
+  final FuchsiaKernelCompiler fuchsiaKernelCompiler;
+
+  @override
+  final FuchsiaDevFinder fuchsiaDevFinder;
+}
diff --git a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_workflow_test.dart b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_workflow_test.dart
new file mode 100644
index 0000000..6b6c973
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_workflow_test.dart
@@ -0,0 +1,58 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
+
+class MockFile extends Mock implements File {}
+
+void main() {
+  group('Fuchsia workflow', () {
+    final MockFile devFinder = MockFile();
+    final MockFile sshConfig = MockFile();
+    when(devFinder.absolute).thenReturn(devFinder);
+    when(sshConfig.absolute).thenReturn(sshConfig);
+
+    testUsingContext(
+        'can not list and launch devices if there is not ssh config and dev finder',
+        () {
+      expect(fuchsiaWorkflow.canLaunchDevices, false);
+      expect(fuchsiaWorkflow.canListDevices, false);
+      expect(fuchsiaWorkflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      FuchsiaArtifacts: () =>
+          FuchsiaArtifacts(devFinder: null, sshConfig: null),
+    });
+
+    testUsingContext(
+        'can not list and launch devices if there is not ssh config and dev finder',
+        () {
+      expect(fuchsiaWorkflow.canLaunchDevices, false);
+      expect(fuchsiaWorkflow.canListDevices, true);
+      expect(fuchsiaWorkflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      FuchsiaArtifacts: () =>
+          FuchsiaArtifacts(devFinder: devFinder, sshConfig: null),
+    });
+
+    testUsingContext(
+        'can list and launch devices supported with sufficient SDK artifacts',
+        () {
+      expect(fuchsiaWorkflow.canLaunchDevices, true);
+      expect(fuchsiaWorkflow.canListDevices, true);
+      expect(fuchsiaWorkflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      FuchsiaArtifacts: () =>
+          FuchsiaArtifacts(devFinder: devFinder, sshConfig: sshConfig),
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
new file mode 100644
index 0000000..2561daf
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -0,0 +1,288 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/devfs.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  group('validateReloadReport', () {
+    testUsingContext('invalid', () async {
+      expect(HotRunner.validateReloadReport(<String, dynamic>{}), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{},
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[
+          ],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <String, dynamic>{
+            'message': 'error',
+          },
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[
+            <String, dynamic>{'message': false},
+          ],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[
+            <String, dynamic>{'message': <String>['error']},
+          ],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[
+            <String, dynamic>{'message': 'error'},
+            <String, dynamic>{'message': <String>['error']},
+          ],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': false,
+        'details': <String, dynamic>{
+          'notices': <Map<String, dynamic>>[
+            <String, dynamic>{'message': 'error'},
+          ],
+        },
+      }), false);
+      expect(HotRunner.validateReloadReport(<String, dynamic>{
+        'type': 'ReloadReport',
+        'success': true,
+      }), true);
+    });
+  });
+
+  group('hotRestart', () {
+    final MockResidentCompiler residentCompiler = MockResidentCompiler();
+    final MockDevFs mockDevFs = MockDevFs();
+    MockLocalEngineArtifacts mockArtifacts;
+
+    when(mockDevFs.update(
+      mainPath: anyNamed('mainPath'),
+      target: anyNamed('target'),
+      bundle: anyNamed('bundle'),
+      firstBuildTime: anyNamed('firstBuildTime'),
+      bundleFirstUpload: anyNamed('bundleFirstUpload'),
+      generator: anyNamed('generator'),
+      fullRestart: anyNamed('fullRestart'),
+      dillOutputPath: anyNamed('dillOutputPath'),
+      trackWidgetCreation: anyNamed('trackWidgetCreation'),
+      projectRootPath: anyNamed('projectRootPath'),
+      pathToReload: anyNamed('pathToReload'),
+      invalidatedFiles: anyNamed('invalidatedFiles'),
+    )).thenAnswer((Invocation _) => Future<UpdateFSReport>.value(
+        UpdateFSReport(success: true, syncedBytes: 1000, invalidatedSourcesCount: 1)));
+    when(mockDevFs.assetPathsToEvict).thenReturn(<String>{});
+    when(mockDevFs.baseUri).thenReturn(Uri.file('test'));
+    when(mockDevFs.sources).thenReturn(<Uri>[Uri.file('test')]);
+    when(mockDevFs.lastCompiled).thenReturn(DateTime.now());
+
+    setUp(() {
+      mockArtifacts = MockLocalEngineArtifacts();
+      when(mockArtifacts.getArtifactPath(Artifact.flutterPatchedSdkPath)).thenReturn('some/path');
+    });
+
+    testUsingContext('Does not hot restart when device does not support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart failed.
+      expect(result.isOk, false);
+      expect(result.message, 'hotRestart not supported');
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+
+    testUsingContext('Does not hot restart when one of many devices does not support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      final MockDevice mockHotDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockHotDevice.supportsHotReload).thenReturn(true);
+      when(mockHotDevice.supportsHotRestart).thenReturn(true);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart failed.
+      expect(result.isOk, false);
+      expect(result.message, 'hotRestart not supported');
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+
+    testUsingContext('Does hot restarts when all devices support it', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      final MockDevice mockHotDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      when(mockHotDevice.supportsHotReload).thenReturn(true);
+      when(mockHotDevice.supportsHotRestart).thenReturn(true);
+      // Trigger a restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+        FlutterDevice(mockHotDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart was successful.
+      expect(result.isOk, true);
+      expect(result.message, isNot('hotRestart not supported'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+
+    testUsingContext('setup function fails', () async {
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      expect(result.isOk, false);
+      expect(result.message, 'setupHotRestart failed');
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: false),
+    });
+
+    testUsingContext('hot restart supported', () async {
+      // Setup mocks
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(true);
+      // Trigger hot restart.
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug)..devFS = mockDevFs,
+      ];
+      final OperationResult result = await HotRunner(devices).restart(fullRestart: true);
+      // Expect hot restart successful.
+      expect(result.isOk, true);
+      expect(result.message, isNot('setupHotRestart failed'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+
+    group('shutdown hook tests', () {
+      TestHotRunnerConfig shutdownTestingConfig;
+
+      setUp(() {
+        shutdownTestingConfig = TestHotRunnerConfig(
+          successfulSetup: true,
+        );
+      });
+
+      testUsingContext('shutdown hook called after signal', () async {
+        final MockDevice mockDevice = MockDevice();
+        when(mockDevice.supportsHotReload).thenReturn(true);
+        when(mockDevice.supportsHotRestart).thenReturn(true);
+        when(mockDevice.supportsFlutterExit).thenReturn(false);
+        final List<FlutterDevice> devices = <FlutterDevice>[
+          FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
+        ];
+        await HotRunner(devices).cleanupAfterSignal();
+        expect(shutdownTestingConfig.shutdownHookCalled, true);
+      }, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        HotRunnerConfig: () => shutdownTestingConfig,
+      });
+
+      testUsingContext('shutdown hook called after app stop', () async {
+        final MockDevice mockDevice = MockDevice();
+        when(mockDevice.supportsHotReload).thenReturn(true);
+        when(mockDevice.supportsHotRestart).thenReturn(true);
+        when(mockDevice.supportsFlutterExit).thenReturn(false);
+        final List<FlutterDevice> devices = <FlutterDevice>[
+          FlutterDevice(mockDevice, generator: residentCompiler, trackWidgetCreation: false, buildMode: BuildMode.debug),
+        ];
+        await HotRunner(devices).preExit();
+        expect(shutdownTestingConfig.shutdownHookCalled, true);
+      }, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        HotRunnerConfig: () => shutdownTestingConfig,
+      });
+    });
+  });
+}
+
+class MockDevFs extends Mock implements DevFS {}
+
+class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
+
+class MockDevice extends Mock implements Device {
+  MockDevice() {
+    when(isSupported()).thenReturn(true);
+  }
+}
+
+class TestHotRunnerConfig extends HotRunnerConfig {
+  TestHotRunnerConfig({@required this.successfulSetup});
+  bool successfulSetup;
+  bool shutdownHookCalled = false;
+
+  @override
+  Future<bool> setupHotRestart() async {
+    return successfulSetup;
+  }
+
+  @override
+  Future<void> runPreShutdownOperations() async {
+    shutdownHookCalled = true;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/intellij/intellij_test.dart b/packages/flutter_tools/test/general.shard/intellij/intellij_test.dart
new file mode 100644
index 0000000..181141c
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/intellij/intellij_test.dart
@@ -0,0 +1,106 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+import 'dart:convert';
+
+import 'package:archive/archive.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/intellij/intellij.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  FileSystem fs;
+
+  void writeFileCreatingDirectories(String path, List<int> bytes) {
+    final File file = fs.file(path);
+    file.parent.createSync(recursive: true);
+    file.writeAsBytesSync(bytes);
+  }
+
+  setUp(() {
+    fs = MemoryFileSystem();
+  });
+
+  group('IntelliJ', () {
+    group('plugins', () {
+      testUsingContext('found', () async {
+        final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath);
+
+        final Archive dartJarArchive =
+            buildSingleFileArchive('META-INF/plugin.xml', r'''
+<idea-plugin version="2">
+  <name>Dart</name>
+  <version>162.2485</version>
+</idea-plugin>
+''');
+        writeFileCreatingDirectories(
+            fs.path.join(_kPluginsPath, 'Dart', 'lib', 'Dart.jar'),
+            ZipEncoder().encode(dartJarArchive));
+
+        final Archive flutterJarArchive =
+            buildSingleFileArchive('META-INF/plugin.xml', r'''
+<idea-plugin version="2">
+  <name>Flutter</name>
+  <version>0.1.3</version>
+</idea-plugin>
+''');
+        writeFileCreatingDirectories(
+            fs.path.join(_kPluginsPath, 'flutter-intellij.jar'),
+            ZipEncoder().encode(flutterJarArchive));
+
+        final List<ValidationMessage> messages = <ValidationMessage>[];
+        plugins.validatePackage(messages, <String>['Dart'], 'Dart');
+        plugins.validatePackage(messages,
+            <String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter',
+            minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
+
+        ValidationMessage message = messages
+            .firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
+        expect(message.message, 'Dart plugin version 162.2485');
+
+        message = messages.firstWhere(
+            (ValidationMessage m) => m.message.startsWith('Flutter '));
+        expect(message.message, contains('Flutter plugin version 0.1.3'));
+        expect(message.message, contains('recommended minimum version'));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+
+      testUsingContext('not found', () async {
+        final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath);
+
+        final List<ValidationMessage> messages = <ValidationMessage>[];
+        plugins.validatePackage(messages, <String>['Dart'], 'Dart');
+        plugins.validatePackage(messages,
+            <String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter',
+            minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
+
+        ValidationMessage message = messages
+            .firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
+        expect(message.message, contains('Dart plugin not installed'));
+
+        message = messages.firstWhere(
+            (ValidationMessage m) => m.message.startsWith('Flutter '));
+        expect(message.message, contains('Flutter plugin not installed'));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+      });
+    });
+  });
+}
+
+const String _kPluginsPath = '/data/intellij/plugins';
+
+Archive buildSingleFileArchive(String path, String content) {
+  final Archive archive = Archive();
+
+  final List<int> bytes = utf8.encode(content);
+  archive.addFile(ArchiveFile(path, bytes.length, bytes));
+
+  return archive;
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart
new file mode 100644
index 0000000..c9a54b3
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart
@@ -0,0 +1,476 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/config.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/ios/code_signing.dart';
+import 'package:flutter_tools/src/globals.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('Auto signing', () {
+    ProcessManager mockProcessManager;
+    Config mockConfig;
+    IosProject mockIosProject;
+    BuildableIOSApp app;
+    AnsiTerminal testTerminal;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockConfig = MockConfig();
+      mockIosProject = MockIosProject();
+      when(mockIosProject.buildSettings).thenReturn(<String, String>{
+        'For our purposes': 'a non-empty build settings map is valid',
+      });
+      testTerminal = TestTerminal();
+      app = BuildableIOSApp(mockIosProject);
+    });
+
+    testUsingContext('No auto-sign if Xcode project settings are not available', () async {
+      when(mockIosProject.buildSettings).thenReturn(null);
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+      expect(signingConfigs, isNull);
+    });
+
+    testUsingContext('No discovery if development team specified in Xcode project', () async {
+      when(mockIosProject.buildSettings).thenReturn(<String, String>{
+        'DEVELOPMENT_TEAM': 'abc',
+      });
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+      expect(signingConfigs, isNull);
+      expect(testLogger.statusText, equals(
+        'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n'
+      ));
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('No auto-sign if security or openssl not available', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsFail);
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+      expect(signingConfigs, isNull);
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('No valid code signing certificates shows instructions', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(exitsHappy);
+
+
+      Map<String, String> signingConfigs;
+      try {
+        signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+        fail('No identity should throw tool error');
+      } on ToolExit {
+        expect(signingConfigs, isNull);
+        expect(testLogger.errorText, contains('No valid code signing certificates were found'));
+      }
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('Test single identity and certificate organization works', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        '''
+1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
+    1 valid identities found''',
+        '',
+      ));
+      when(mockProcessManager.runSync(
+        <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'],
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        'This is a mock certificate',
+        '',
+      ));
+
+      final MockProcess mockProcess = MockProcess();
+      final MockStdIn mockStdIn = MockStdIn();
+      final MockStream mockStdErr = MockStream();
+
+      when(mockProcessManager.start(
+      argThat(contains('openssl')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) => Future<Process>.value(mockProcess));
+
+      when(mockProcess.stdin).thenReturn(mockStdIn);
+      when(mockProcess.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US'
+            ))
+          ));
+      when(mockProcess.stderr).thenAnswer((Invocation invocation) => mockStdErr);
+      when(mockProcess.exitCode).thenAnswer((_) async => 0);
+
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+
+      expect(testLogger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)'));
+      expect(testLogger.errorText, isEmpty);
+      verify(mockStdIn.write('This is a mock certificate'));
+      expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '3333CCCC33'});
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('Test multiple identity and certificate organization works', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        '''
+1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
+2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
+3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
+    3 valid identities found''',
+        '',
+      ));
+      mockTerminalStdInStream =
+          Stream<String>.fromFuture(Future<String>.value('3'));
+      when(mockProcessManager.runSync(
+        <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'],
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        'This is a mock certificate',
+        '',
+      ));
+
+      final MockProcess mockOpenSslProcess = MockProcess();
+      final MockStdIn mockOpenSslStdIn = MockStdIn();
+      final MockStream mockOpenSslStdErr = MockStream();
+
+      when(mockProcessManager.start(
+      argThat(contains('openssl')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) => Future<Process>.value(mockOpenSslProcess));
+
+      when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
+      when(mockOpenSslProcess.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US'
+            ))
+          ));
+      when(mockOpenSslProcess.stderr).thenAnswer((Invocation invocation) => mockOpenSslStdErr);
+      when(mockOpenSslProcess.exitCode).thenAnswer((_) => Future<int>.value(0));
+
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+
+      expect(
+        testLogger.statusText,
+        contains('Please select a certificate for code signing [<bold>1</bold>|2|3|a]: 3'),
+      );
+      expect(
+        testLogger.statusText,
+        contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"'),
+      );
+      expect(testLogger.errorText, isEmpty);
+      verify(mockOpenSslStdIn.write('This is a mock certificate'));
+      expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '4444DDDD44'});
+
+      verify(config.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'));
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AnsiTerminal: () => testTerminal,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('Test multiple identity in machine mode works', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        '''
+1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
+2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
+3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
+    3 valid identities found''',
+          '',
+      ));
+      mockTerminalStdInStream =
+        Stream<String>.fromFuture(Future<String>.error(Exception('Cannot read from StdIn')));
+      when(mockProcessManager.runSync(
+        <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'],
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        'This is a mock certificate',
+        '',
+      ));
+
+      final MockProcess mockOpenSslProcess = MockProcess();
+      final MockStdIn mockOpenSslStdIn = MockStdIn();
+      final MockStream mockOpenSslStdErr = MockStream();
+
+      when(mockProcessManager.start(
+      argThat(contains('openssl')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) => Future<Process>.value(mockOpenSslProcess));
+
+      when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
+      when(mockOpenSslProcess.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=5555EEEE55/O=My Team/C=US'
+            )),
+          ));
+      when(mockOpenSslProcess.stderr).thenAnswer((Invocation invocation) => mockOpenSslStdErr);
+      when(mockOpenSslProcess.exitCode).thenAnswer((_) => Future<int>.value(0));
+
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: false);
+
+      expect(
+        testLogger.statusText,
+        contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 1 (1111AAAA11)"'),
+      );
+      expect(testLogger.errorText, isEmpty);
+      verify(mockOpenSslStdIn.write('This is a mock certificate'));
+      expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '5555EEEE55'});
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AnsiTerminal: () => testTerminal,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('Test saved certificate used', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        '''
+1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
+2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
+3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
+    3 valid identities found''',
+        '',
+      ));
+      when(mockProcessManager.runSync(
+        <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'],
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        'This is a mock certificate',
+        '',
+      ));
+
+      final MockProcess mockOpenSslProcess = MockProcess();
+      final MockStdIn mockOpenSslStdIn = MockStdIn();
+      final MockStream mockOpenSslStdErr = MockStream();
+
+      when(mockProcessManager.start(
+      argThat(contains('openssl')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) => Future<Process>.value(mockOpenSslProcess));
+
+      when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
+      when(mockOpenSslProcess.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US'
+            ))
+          ));
+      when(mockOpenSslProcess.stderr).thenAnswer((Invocation invocation) => mockOpenSslStdErr);
+      when(mockOpenSslProcess.exitCode).thenAnswer((_) => Future<int>.value(0));
+      when<String>(mockConfig.getValue('ios-signing-cert')).thenReturn('iPhone Developer: Profile 3 (3333CCCC33)');
+
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+
+      expect(
+        testLogger.statusText,
+        contains('Found saved certificate choice "iPhone Developer: Profile 3 (3333CCCC33)". To clear, use "flutter config"'),
+      );
+      expect(
+        testLogger.statusText,
+        contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"'),
+      );
+      expect(testLogger.errorText, isEmpty);
+      verify(mockOpenSslStdIn.write('This is a mock certificate'));
+      expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '4444DDDD44'});
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      OutputPreferences: () => OutputPreferences(wrapText: false),
+    });
+
+    testUsingContext('Test invalid saved certificate shows error and prompts again', () async {
+      when(mockProcessManager.runSync(<String>['which', 'security']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(<String>['which', 'openssl']))
+          .thenReturn(exitsHappy);
+      when(mockProcessManager.runSync(
+      argThat(contains('find-identity')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        '''
+1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
+2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
+3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
+    3 valid identities found''',
+        '',
+      ));
+      mockTerminalStdInStream =
+          Stream<String>.fromFuture(Future<String>.value('3'));
+      when(mockProcessManager.runSync(
+        <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'],
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenReturn(ProcessResult(
+        1, // pid
+        0, // exitCode
+        'This is a mock certificate',
+        '',
+      ));
+
+
+      final MockProcess mockOpenSslProcess = MockProcess();
+      final MockStdIn mockOpenSslStdIn = MockStdIn();
+      final MockStream mockOpenSslStdErr = MockStream();
+
+      when(mockProcessManager.start(
+      argThat(contains('openssl')),
+        environment: anyNamed('environment'),
+        workingDirectory: anyNamed('workingDirectory'),
+      )).thenAnswer((Invocation invocation) => Future<Process>.value(mockOpenSslProcess));
+
+      when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
+      when(mockOpenSslProcess.stdout)
+          .thenAnswer((Invocation invocation) => Stream<List<int>>.fromFuture(
+            Future<List<int>>.value(utf8.encode(
+              'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US'
+            ))
+          ));
+      when(mockOpenSslProcess.stderr).thenAnswer((Invocation invocation) => mockOpenSslStdErr);
+      when(mockOpenSslProcess.exitCode).thenAnswer((_) => Future<int>.value(0));
+      when<String>(mockConfig.getValue('ios-signing-cert')).thenReturn('iPhone Developer: Invalid Profile');
+
+      final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
+
+      expect(
+        testLogger.errorText,
+        contains('Saved signing certificate "iPhone Developer: Invalid Profile" is not a valid development certificate'),
+      );
+      expect(
+        testLogger.statusText,
+        contains('Certificate choice "iPhone Developer: Profile 3 (3333CCCC33)"'),
+      );
+      expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '4444DDDD44'});
+      verify(config.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'));
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      Config: () => mockConfig,
+      AnsiTerminal: () => testTerminal,
+    });
+  });
+}
+
+final ProcessResult exitsHappy = ProcessResult(
+  1, // pid
+  0, // exitCode
+  '', // stdout
+  '', // stderr
+);
+
+final ProcessResult exitsFail = ProcessResult(
+  2, // pid
+  1, // exitCode
+  '', // stdout
+  '', // stderr
+);
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcess extends Mock implements Process {}
+class MockStream extends Mock implements Stream<List<int>> {}
+class MockStdIn extends Mock implements IOSink {}
+class MockConfig extends Mock implements Config {}
+
+Stream<String> mockTerminalStdInStream;
+
+class TestTerminal extends AnsiTerminal {
+  @override
+  String bolden(String message) => '<bold>$message</bold>';
+
+  @override
+  Stream<String> get keystrokes {
+    return mockTerminalStdInStream;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
new file mode 100644
index 0000000..0716212
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -0,0 +1,228 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+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/device.dart';
+import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+class MockIMobileDevice extends Mock implements IMobileDevice {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcode extends Mock implements Xcode {}
+class MockFile extends Mock implements File {}
+class MockProcess extends Mock implements Process {}
+
+void main() {
+  final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform());
+  osx.operatingSystem = 'macos';
+
+  group('getAttachedDevices', () {
+    MockIMobileDevice mockIMobileDevice;
+
+    setUp(() {
+      mockIMobileDevice = MockIMobileDevice();
+    });
+
+    testUsingContext('return no devices if Xcode is not installed', () async {
+      when(mockIMobileDevice.isInstalled).thenReturn(false);
+      expect(await IOSDevice.getAttachedDevices(), isEmpty);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+
+    testUsingContext('returns no devices if none are attached', () async {
+      when(iMobileDevice.isInstalled).thenReturn(true);
+      when(iMobileDevice.getAvailableDeviceIDs())
+          .thenAnswer((Invocation invocation) => Future<String>.value(''));
+      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
+      expect(devices, isEmpty);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+
+    testUsingContext('returns attached devices', () async {
+      when(iMobileDevice.isInstalled).thenReturn(true);
+      when(iMobileDevice.getAvailableDeviceIDs())
+          .thenAnswer((Invocation invocation) => Future<String>.value('''
+98206e7a4afd4aedaff06e687594e089dede3c44
+f577a7903cc54959be2e34bc4f7f80b7009efcf4
+'''));
+      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
+          .thenAnswer((_) => Future<String>.value('La tele me regarde'));
+      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'ProductVersion'))
+          .thenAnswer((_) => Future<String>.value('10.3.2'));
+      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
+          .thenAnswer((_) => Future<String>.value('Puits sans fond'));
+      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'ProductVersion'))
+          .thenAnswer((_) => Future<String>.value('11.0'));
+      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
+      expect(devices, hasLength(2));
+      expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
+      expect(devices[0].name, 'La tele me regarde');
+      expect(devices[1].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
+      expect(devices[1].name, 'Puits sans fond');
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+
+    testUsingContext('returns attached devices and ignores devices that cannot be found by ideviceinfo', () async {
+      when(iMobileDevice.isInstalled).thenReturn(true);
+      when(iMobileDevice.getAvailableDeviceIDs())
+          .thenAnswer((Invocation invocation) => Future<String>.value('''
+98206e7a4afd4aedaff06e687594e089dede3c44
+f577a7903cc54959be2e34bc4f7f80b7009efcf4
+'''));
+      when(iMobileDevice.getInfoForDevice('98206e7a4afd4aedaff06e687594e089dede3c44', 'DeviceName'))
+          .thenAnswer((_) => Future<String>.value('La tele me regarde'));
+      when(iMobileDevice.getInfoForDevice('f577a7903cc54959be2e34bc4f7f80b7009efcf4', 'DeviceName'))
+          .thenThrow(IOSDeviceNotFoundError('Device not found'));
+      final List<IOSDevice> devices = await IOSDevice.getAttachedDevices();
+      expect(devices, hasLength(1));
+      expect(devices[0].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
+      expect(devices[0].name, 'La tele me regarde');
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+  });
+
+  group('decodeSyslog', () {
+    test('decodes a syslog-encoded line', () {
+      final String decoded = decodeSyslog(r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!');
+      expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!');
+    });
+
+    test('passes through un-decodeable lines as-is', () {
+      final String decoded = decodeSyslog(r'I \M-b\M^O syslog!');
+      expect(decoded, r'I \M-b\M^O syslog!');
+    });
+  });
+  group('logging', () {
+    MockIMobileDevice mockIMobileDevice;
+    MockIosProject mockIosProject;
+
+    setUp(() {
+      mockIMobileDevice = MockIMobileDevice();
+      mockIosProject = MockIosProject();
+    });
+
+    testUsingContext('suppresses non-Flutter lines from output', () async {
+      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
+        final Process mockProcess = MockProcess();
+        when(mockProcess.stdout).thenAnswer((Invocation invocation) =>
+            Stream<List<int>>.fromIterable(<List<int>>['''
+  Runner(Flutter)[297] <Notice>: A is for ari
+  Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestaltSupport.m:153: pid 123 (Runner) does not have sandbox access for frZQaeyWLUvLjeuEK43hmg and IS NOT appropriately entitled
+  Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestalt.c:550: no access to InverseDeviceID (see <rdar://problem/11744455>)
+  Runner(Flutter)[297] <Notice>: I is for ichigo
+  Runner(UIKit)[297] <Notice>: E is for enpitsu"
+  '''.codeUnits]));
+        when(mockProcess.stderr)
+            .thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+        // Delay return of exitCode until after stdout stream data, since it terminates the logger.
+        when(mockProcess.exitCode)
+            .thenAnswer((Invocation invocation) => Future<int>.delayed(Duration.zero, () => 0));
+        return Future<Process>.value(mockProcess);
+      });
+
+      final IOSDevice device = IOSDevice('123456');
+      final DeviceLogReader logReader = device.getLogReader(
+        app: BuildableIOSApp(mockIosProject),
+      );
+
+      final List<String> lines = await logReader.logLines.toList();
+      expect(lines, <String>['A is for ari', 'I is for ichigo']);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+
+    testUsingContext('includes multi-line Flutter logs in the output', () async {
+      when(mockIMobileDevice.startLogger('123456')).thenAnswer((Invocation invocation) {
+        final Process mockProcess = MockProcess();
+        when(mockProcess.stdout).thenAnswer((Invocation invocation) =>
+            Stream<List<int>>.fromIterable(<List<int>>['''
+  Runner(Flutter)[297] <Notice>: This is a multi-line message,
+  with another Flutter message following it.
+  Runner(Flutter)[297] <Notice>: This is a multi-line message,
+  with a non-Flutter log message following it.
+  Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
+  '''.codeUnits]));
+        when(mockProcess.stderr)
+            .thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+        // Delay return of exitCode until after stdout stream data, since it terminates the logger.
+        when(mockProcess.exitCode)
+            .thenAnswer((Invocation invocation) => Future<int>.delayed(Duration.zero, () => 0));
+        return Future<Process>.value(mockProcess);
+      });
+
+      final IOSDevice device = IOSDevice('123456');
+      final DeviceLogReader logReader = device.getLogReader(
+        app: BuildableIOSApp(mockIosProject),
+      );
+
+      final List<String> lines = await logReader.logLines.toList();
+      expect(lines, <String>[
+        'This is a multi-line message,',
+        '  with another Flutter message following it.',
+        'This is a multi-line message,',
+        '  with a non-Flutter log message following it.',
+      ]);
+      expect(device.category, Category.mobile);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => mockIMobileDevice,
+    });
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
+    fs.file('pubspec.yaml')
+      ..createSync()
+      ..writeAsStringSync(r'''
+name: example
+
+flutter:
+  module: {}
+''');
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.directory('ios').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSDevice('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSDevice('test').isSupportedForProject(flutterProject), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart
new file mode 100644
index 0000000..2ac1019
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart
@@ -0,0 +1,178 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('iOS Workflow validation', () {
+    MockIMobileDevice iMobileDevice;
+    MockIMobileDevice iMobileDeviceUninstalled;
+    MockProcessManager processManager;
+    FileSystem fs;
+
+    setUp(() {
+      iMobileDevice = MockIMobileDevice();
+      iMobileDeviceUninstalled = MockIMobileDevice(isInstalled: false);
+      processManager = MockProcessManager();
+      fs = MemoryFileSystem();
+    });
+
+    testUsingContext('Emit missing status when nothing is installed', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(
+        hasHomebrew: false,
+        hasIosDeploy: false,
+        hasIDeviceInstaller: false,
+        iosDeployVersionText: '0.0.0',
+      );
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDeviceUninstalled,
+    });
+
+    testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasHomebrew: false);
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDevice,
+    });
+
+    testUsingContext('Emits partial status when libimobiledevice is not installed', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => MockIMobileDevice(isInstalled: false, isWorking: false),
+    });
+
+    testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => MockIMobileDevice(isWorking: false),
+    });
+
+    testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
+      when(processManager.run(
+        <String>['ideviceinfo', '-u', '00008020-001C2D903C42002E'],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')),
+      ).thenAnswer((Invocation _) async {
+        final MockProcessResult result = MockProcessResult();
+        when<String>(result.stdout).thenReturn(r'''
+Usage: ideviceinfo [OPTIONS]
+Show information about a connected device.
+
+  -d, --debug		enable communication debugging
+  -s, --simple		use a simple connection to avoid auto-pairing with the device
+  -u, --udid UDID	target specific device by its 40-digit device UDID
+  -q, --domain NAME	set domain of query to NAME. Default: None
+  -k, --key NAME	only query key specified by NAME. Default: All keys.
+  -x, --xml		output information as xml plist instead of key/value pairs
+  -h, --help		prints usage information
+        ''');
+        return null;
+      });
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+
+
+    testUsingContext('Emits partial status when ios-deploy is not installed', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasIosDeploy: false);
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDevice,
+    });
+
+    testUsingContext('Emits partial status when ios-deploy version is too low', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0');
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDevice,
+    });
+
+    testUsingContext('Emits partial status when ios-deploy version is a known bad version', () async {
+      final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '2.0.0');
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDevice,
+    });
+
+    testUsingContext('Succeeds when all checks pass', () async {
+      final ValidationResult result = await IOSWorkflowTestTarget().validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      IMobileDevice: () => iMobileDevice,
+      ProcessManager: () => processManager,
+    });
+  });
+}
+
+final ProcessResult exitsHappy = ProcessResult(
+  1, // pid
+  0, // exitCode
+  '', // stdout
+  '', // stderr
+);
+
+class MockIMobileDevice extends IMobileDevice {
+  MockIMobileDevice({
+    this.isInstalled = true,
+    bool isWorking = true,
+  }) : isWorking = Future<bool>.value(isWorking);
+
+  @override
+  final bool isInstalled;
+
+  @override
+  final Future<bool> isWorking;
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcessResult extends Mock implements ProcessResult {}
+
+class IOSWorkflowTestTarget extends IOSValidator {
+  IOSWorkflowTestTarget({
+    this.hasHomebrew = true,
+    bool hasIosDeploy = true,
+    String iosDeployVersionText = '1.9.4',
+    bool hasIDeviceInstaller = true,
+  }) : hasIosDeploy = Future<bool>.value(hasIosDeploy),
+       iosDeployVersionText = Future<String>.value(iosDeployVersionText),
+       hasIDeviceInstaller = Future<bool>.value(hasIDeviceInstaller);
+
+  @override
+  final bool hasHomebrew;
+
+  @override
+  final Future<bool> hasIosDeploy;
+
+  @override
+  final Future<String> iosDeployVersionText;
+
+  @override
+  final Future<bool> hasIDeviceInstaller;
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
new file mode 100644
index 0000000..e9f88c7
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart
@@ -0,0 +1,334 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+final Generator _kNoColorTerminalPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false;
+final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
+  Platform: _kNoColorTerminalPlatform,
+};
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockFile extends Mock implements File {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
+class MockIosProject extends Mock implements IosProject {}
+
+void main() {
+  group('IMobileDevice', () {
+    final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform())
+      ..operatingSystem = 'macos';
+    MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+    });
+
+    testUsingContext('getAvailableDeviceIDs throws ToolExit when libimobiledevice is not installed', () async {
+      when(mockProcessManager.run(<String>['idevice_id', '-l']))
+          .thenThrow(const ProcessException('idevice_id', <String>['-l']));
+      expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async {
+      when(mockProcessManager.run(<String>['idevice_id', '-l']))
+          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', 'Sad today')));
+      expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async {
+      when(mockProcessManager.run(<String>['idevice_id', '-l']))
+          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'foo', '')));
+      expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo');
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when ideviceinfo returns specific error code and message', () async {
+      when(mockProcessManager.run(<String>['ideviceinfo', '-u', 'foo', '-k', 'bar']))
+          .thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', '')));
+      expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf<IOSDeviceNotFoundError>()));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    group('screenshot', () {
+      final String outputPath = fs.path.join('some', 'test', 'path', 'image.png');
+      MockProcessManager mockProcessManager;
+      MockFile mockOutputFile;
+
+      setUp(() {
+        mockProcessManager = MockProcessManager();
+        mockOutputFile = MockFile();
+      });
+
+      testUsingContext('error if idevicescreenshot is not installed', () async {
+        when(mockOutputFile.path).thenReturn(outputPath);
+
+        // Let `idevicescreenshot` fail with exit code 1.
+        when(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
+            environment: null,
+            workingDirectory: null,
+        )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(4, 1, '', '')));
+
+        expect(() async => await iMobileDevice.takeScreenshot(mockOutputFile), throwsA(anything));
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        Platform: () => osx,
+      });
+
+      testUsingContext('idevicescreenshot captures and returns screenshot', () async {
+        when(mockOutputFile.path).thenReturn(outputPath);
+        when(mockProcessManager.run(any, environment: null, workingDirectory: null)).thenAnswer(
+            (Invocation invocation) => Future<ProcessResult>.value(ProcessResult(4, 0, '', '')));
+
+        await iMobileDevice.takeScreenshot(mockOutputFile);
+        verify(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
+            environment: null,
+            workingDirectory: null,
+        ));
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+      });
+    });
+  });
+
+  group('Diagnose Xcode build failure', () {
+    Map<String, String> buildSettings;
+
+    setUp(() {
+      buildSettings = <String, String>{
+        'PRODUCT_BUNDLE_IDENTIFIER': 'test.app',
+      };
+    });
+
+    testUsingContext('No provisioning profile shows message', () async {
+      final XcodeBuildResult buildResult = XcodeBuildResult(
+        success: false,
+        stdout: '''
+Launching lib/main.dart on iPhone in debug mode...
+Signing iOS app for device deployment using developer identity: "iPhone Developer: test@flutter.io (1122334455)"
+Running Xcode build...                                1.3s
+Failed to build iOS app
+Error output from Xcode build:
+↳
+    ** BUILD FAILED **
+
+
+    The following build commands failed:
+    	Check dependencies
+    (1 failure)
+Xcode's output:
+↳
+    Build settings from command line:
+        ARCHS = arm64
+        BUILD_DIR = /Users/blah/blah
+        DEVELOPMENT_TEAM = AABBCCDDEE
+        ONLY_ACTIVE_ARCH = YES
+        SDKROOT = iphoneos10.3
+
+    === CLEAN TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===
+
+    Check dependencies
+    [BCEROR]No profiles for 'com.example.test' were found:  Xcode couldn't find a provisioning profile matching 'com.example.test'.
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+
+    Create product structure
+    /bin/mkdir -p /Users/blah/Runner.app
+
+    Clean.Remove clean /Users/blah/Runner.app.dSYM
+        builtin-rm -rf /Users/blah/Runner.app.dSYM
+
+    Clean.Remove clean /Users/blah/Runner.app
+        builtin-rm -rf /Users/blah/Runner.app
+
+    Clean.Remove clean /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build
+        builtin-rm -rf /Users/blah/Runner-dfvicjniknvzghgwsthwtgcjhtsk/Build/Intermediates/Runner.build/Release-iphoneos/Runner.build
+
+    ** CLEAN SUCCEEDED **
+
+    === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===
+
+    Check dependencies
+    No profiles for 'com.example.test' were found:  Xcode couldn't find a provisioning profile matching 'com.example.test'.
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+
+Could not build the precompiled application for the device.
+
+Error launching application on iPhone.''',
+        xcodeBuildExecution: XcodeBuildExecution(
+          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
+          appDirectory: '/blah/blah',
+          buildForPhysicalDevice: true,
+          buildSettings: buildSettings,
+        ),
+      );
+
+      await diagnoseXcodeBuildFailure(buildResult);
+      expect(
+        testLogger.errorText,
+        contains('No Provisioning Profile was found for your project\'s Bundle Identifier or your \ndevice.'),
+      );
+    }, overrides: noColorTerminalOverride);
+
+    testUsingContext('No development team shows message', () async {
+      final XcodeBuildResult buildResult = XcodeBuildResult(
+        success: false,
+        stdout: '''
+Running "flutter pub get" in flutter_gallery...  0.6s
+Launching lib/main.dart on x in release mode...
+Running pod install...                                1.2s
+Running Xcode build...                                1.4s
+Failed to build iOS app
+Error output from Xcode build:
+↳
+    ** BUILD FAILED **
+
+
+    The following build commands failed:
+    	Check dependencies
+    (1 failure)
+Xcode's output:
+↳
+    blah
+
+    === CLEAN TARGET url_launcher OF PROJECT Pods WITH CONFIGURATION Release ===
+
+    Check dependencies
+
+    blah
+
+    === CLEAN TARGET Pods-Runner OF PROJECT Pods WITH CONFIGURATION Release ===
+
+    Check dependencies
+
+    blah
+
+    === CLEAN TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===
+
+    Check dependencies
+    [BCEROR]Signing for "Runner" requires a development team. Select a development team in the project editor.
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    [BCEROR]Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+
+    blah
+
+    ** CLEAN SUCCEEDED **
+
+    === BUILD TARGET url_launcher OF PROJECT Pods WITH CONFIGURATION Release ===
+
+    Check dependencies
+
+    blah
+
+    === BUILD TARGET Pods-Runner OF PROJECT Pods WITH CONFIGURATION Release ===
+
+    Check dependencies
+
+    blah
+
+    === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Release ===
+
+    Check dependencies
+    Signing for "Runner" requires a development team. Select a development team in the project editor.
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+    Code signing is required for product type 'Application' in SDK 'iOS 10.3'
+
+Could not build the precompiled application for the device.''',
+        xcodeBuildExecution: XcodeBuildExecution(
+          buildCommands: <String>['xcrun', 'xcodebuild', 'blah'],
+          appDirectory: '/blah/blah',
+          buildForPhysicalDevice: true,
+          buildSettings: buildSettings,
+        ),
+      );
+
+      await diagnoseXcodeBuildFailure(buildResult);
+      expect(
+        testLogger.errorText,
+        contains('Building a deployable iOS app requires a selected Development Team with a \nProvisioning Profile.'),
+      );
+    }, overrides: noColorTerminalOverride);
+  });
+
+  group('Upgrades project.pbxproj for old asset usage', () {
+    const List<String> flutterAssetPbxProjLines = <String>[
+      '/* flutter_assets */',
+      '/* App.framework',
+      'another line',
+    ];
+
+    const List<String> appFlxPbxProjLines = <String>[
+      '/* app.flx',
+      '/* App.framework',
+      'another line',
+    ];
+
+    const List<String> cleanPbxProjLines = <String>[
+      '/* App.framework',
+      'another line',
+    ];
+
+    testUsingContext('upgradePbxProjWithFlutterAssets', () async {
+      final MockIosProject project = MockIosProject();
+      final MockFile pbxprojFile = MockFile();
+
+      when(project.xcodeProjectInfoFile).thenReturn(pbxprojFile);
+      when(project.hostAppBundleName).thenReturn('UnitTestRunner.app');
+      when(pbxprojFile.readAsLines())
+          .thenAnswer((_) => Future<List<String>>.value(flutterAssetPbxProjLines));
+      when(pbxprojFile.exists())
+          .thenAnswer((_) => Future<bool>.value(true));
+
+      bool result = await upgradePbxProjWithFlutterAssets(project);
+      expect(result, true);
+      expect(
+        testLogger.statusText,
+        contains('Removing obsolete reference to flutter_assets'),
+      );
+      testLogger.clear();
+
+      when(pbxprojFile.readAsLines())
+          .thenAnswer((_) => Future<List<String>>.value(appFlxPbxProjLines));
+      result = await upgradePbxProjWithFlutterAssets(project);
+      expect(result, true);
+      expect(
+        testLogger.statusText,
+        contains('Removing obsolete reference to app.flx'),
+      );
+      testLogger.clear();
+
+      when(pbxprojFile.readAsLines())
+          .thenAnswer((_) => Future<List<String>>.value(cleanPbxProjLines));
+      result = await upgradePbxProjWithFlutterAssets(project);
+      expect(result, true);
+      expect(
+        testLogger.statusText,
+        isEmpty,
+      );
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
new file mode 100644
index 0000000..59aca13
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart
@@ -0,0 +1,495 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' show ProcessResult, Process;
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/simulators.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+class MockFile extends Mock implements File {}
+class MockIMobileDevice extends Mock implements IMobileDevice {}
+class MockProcess extends Mock implements Process {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcode extends Mock implements Xcode {}
+class MockSimControl extends Mock implements SimControl {}
+class MockIOSWorkflow extends Mock implements IOSWorkflow {}
+
+void main() {
+  FakePlatform osx;
+
+  setUp(() {
+    osx = FakePlatform.fromPlatform(const LocalPlatform());
+    osx.operatingSystem = 'macos';
+  });
+
+  group('logFilePath', () {
+    testUsingContext('defaults to rooted from HOME', () {
+      osx.environment['HOME'] = '/foo/bar';
+      expect(IOSSimulator('123').logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log');
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    }, testOn: 'posix');
+
+    testUsingContext('respects IOS_SIMULATOR_LOG_FILE_PATH', () {
+      osx.environment['HOME'] = '/foo/bar';
+      osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = '/baz/qux/%{id}/system.log';
+      expect(IOSSimulator('456').logFilePath, '/baz/qux/456/system.log');
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+  });
+
+  group('compareIosVersions', () {
+    test('compares correctly', () {
+      // This list must be sorted in ascending preference order
+      final List<String> testList = <String>[
+        '8', '8.0', '8.1', '8.2',
+        '9', '9.0', '9.1', '9.2',
+        '10', '10.0', '10.1',
+      ];
+
+      for (int i = 0; i < testList.length; i++) {
+        expect(compareIosVersions(testList[i], testList[i]), 0);
+      }
+
+      for (int i = 0; i < testList.length - 1; i++) {
+        for (int j = i + 1; j < testList.length; j++) {
+          expect(compareIosVersions(testList[i], testList[j]), lessThan(0));
+          expect(compareIosVersions(testList[j], testList[i]), greaterThan(0));
+        }
+      }
+    });
+  });
+
+  group('compareIphoneVersions', () {
+    test('compares correctly', () {
+      // This list must be sorted in ascending preference order
+      final List<String> testList = <String>[
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-4s',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-5',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-5s',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-6strange',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-6-Plus',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-6',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus',
+        'com.apple.CoreSimulator.SimDeviceType.iPhone-6s',
+      ];
+
+      for (int i = 0; i < testList.length; i++) {
+        expect(compareIphoneVersions(testList[i], testList[i]), 0);
+      }
+
+      for (int i = 0; i < testList.length - 1; i++) {
+        for (int j = i + 1; j < testList.length; j++) {
+          expect(compareIphoneVersions(testList[i], testList[j]), lessThan(0));
+          expect(compareIphoneVersions(testList[j], testList[i]), greaterThan(0));
+        }
+      }
+    });
+  });
+
+  group('sdkMajorVersion', () {
+    // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta.
+    test('can be parsed from iOS-11-3', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3');
+
+      expect(await device.sdkMajorVersion, 11);
+    });
+
+    test('can be parsed from iOS 11.2', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
+
+      expect(await device.sdkMajorVersion, 11);
+    });
+
+    test('Has a simulator category', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
+
+      expect(device.category, Category.mobile);
+    });
+  });
+
+  group('IOSSimulator.isSupported', () {
+    testUsingContext('Apple TV is unsupported', () {
+      expect(IOSSimulator('x', name: 'Apple TV').isSupported(), false);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('Apple Watch is unsupported', () {
+      expect(IOSSimulator('x', name: 'Apple Watch').isSupported(), false);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPad 2 is supported', () {
+      expect(IOSSimulator('x', name: 'iPad 2').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPad Retina is supported', () {
+      expect(IOSSimulator('x', name: 'iPad Retina').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPhone 5 is supported', () {
+      expect(IOSSimulator('x', name: 'iPhone 5').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPhone 5s is supported', () {
+      expect(IOSSimulator('x', name: 'iPhone 5s').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPhone SE is supported', () {
+      expect(IOSSimulator('x', name: 'iPhone SE').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPhone 7 Plus is supported', () {
+      expect(IOSSimulator('x', name: 'iPhone 7 Plus').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+
+    testUsingContext('iPhone X is supported', () {
+      expect(IOSSimulator('x', name: 'iPhone X').isSupported(), true);
+    }, overrides: <Type, Generator>{
+      Platform: () => osx,
+    });
+  });
+
+  group('Simulator screenshot', () {
+    MockXcode mockXcode;
+    MockProcessManager mockProcessManager;
+    IOSSimulator deviceUnderTest;
+
+    setUp(() {
+      mockXcode = MockXcode();
+      mockProcessManager = MockProcessManager();
+      // Let everything else return exit code 0 so process.dart doesn't crash.
+      when(
+        mockProcessManager.run(any, environment: null, workingDirectory: null)
+      ).thenAnswer((Invocation invocation) =>
+        Future<ProcessResult>.value(ProcessResult(2, 0, '', ''))
+      );
+      // Doesn't matter what the device is.
+      deviceUnderTest = IOSSimulator('x', name: 'iPhone SE');
+    });
+
+    testUsingContext(
+      'old Xcode doesn\'t support screenshot',
+      () {
+        when(mockXcode.majorVersion).thenReturn(7);
+        when(mockXcode.minorVersion).thenReturn(1);
+        expect(deviceUnderTest.supportsScreenshot, false);
+      },
+      overrides: <Type, Generator>{Xcode: () => mockXcode},
+    );
+
+    testUsingContext(
+      'Xcode 8.2+ supports screenshots',
+      () async {
+        when(mockXcode.majorVersion).thenReturn(8);
+        when(mockXcode.minorVersion).thenReturn(2);
+        expect(deviceUnderTest.supportsScreenshot, true);
+        final MockFile mockFile = MockFile();
+        when(mockFile.path).thenReturn(fs.path.join('some', 'path', 'to', 'screenshot.png'));
+        await deviceUnderTest.takeScreenshot(mockFile);
+        verify(mockProcessManager.run(
+          <String>[
+              '/usr/bin/xcrun',
+              'simctl',
+              'io',
+              'x',
+              'screenshot',
+              fs.path.join('some', 'path', 'to', 'screenshot.png'),
+          ],
+          environment: null,
+          workingDirectory: null,
+        ));
+      },
+      overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        // Test a real one. Screenshot doesn't require instance states.
+        SimControl: () => SimControl(),
+        Xcode: () => mockXcode,
+      },
+    );
+  });
+
+  group('launchDeviceLogTool', () {
+    MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      when(mockProcessManager.start(any, environment: null, workingDirectory: null))
+        .thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
+    });
+
+    testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
+      await launchDeviceLogTool(device);
+      expect(
+        verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
+        contains('tail'),
+      );
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
+      await launchDeviceLogTool(device);
+      expect(
+        verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
+        contains('/usr/bin/log'),
+      );
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('launchSystemLogTool', () {
+    MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      when(mockProcessManager.start(any, environment: null, workingDirectory: null))
+        .thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
+    });
+
+    testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
+      await launchSystemLogTool(device);
+      expect(
+        verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
+        contains('tail'),
+      );
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
+      await launchSystemLogTool(device);
+      verifyNever(mockProcessManager.start(any, environment: null, workingDirectory: null));
+    },
+    overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('log reader', () {
+    MockProcessManager mockProcessManager;
+    MockIosProject mockIosProject;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockIosProject = MockIosProject();
+    });
+
+    testUsingContext('simulator can output `)`', () async {
+      when(mockProcessManager.start(any, environment: null, workingDirectory: null))
+        .thenAnswer((Invocation invocation) {
+          final Process mockProcess = MockProcess();
+          when(mockProcess.stdout)
+            .thenAnswer((Invocation invocation) {
+              return Stream<List<int>>.fromIterable(<List<int>>['''
+2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) Observatory listening on http://127.0.0.1:57701/
+2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) ))))))))))
+2017-09-13 15:26:57.228948-0700  localhost Runner[37195]: (Flutter) #0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)'''
+                .codeUnits]);
+            });
+          when(mockProcess.stderr)
+              .thenAnswer((Invocation invocation) => const Stream<List<int>>.empty());
+          // Delay return of exitCode until after stdout stream data, since it terminates the logger.
+          when(mockProcess.exitCode)
+              .thenAnswer((Invocation invocation) => Future<int>.delayed(Duration.zero, () => 0));
+          return Future<Process>.value(mockProcess);
+        });
+
+      final IOSSimulator device = IOSSimulator('123456', simulatorCategory: 'iOS 11.0');
+      final DeviceLogReader logReader = device.getLogReader(
+        app: BuildableIOSApp(mockIosProject),
+      );
+
+      final List<String> lines = await logReader.logLines.toList();
+      expect(lines, <String>[
+        'Observatory listening on http://127.0.0.1:57701/',
+        '))))))))))',
+        '#0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)',
+      ]);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('SimControl', () {
+    const int mockPid = 123;
+    const String validSimControlOutput = '''
+{
+  "devices" : {
+    "watchOS 4.3" : [
+      {
+        "state" : "Shutdown",
+        "availability" : "(available)",
+        "name" : "Apple Watch - 38mm",
+        "udid" : "TEST-WATCH-UDID"
+      }
+    ],
+    "iOS 11.4" : [
+      {
+        "state" : "Booted",
+        "availability" : "(available)",
+        "name" : "iPhone 5s",
+        "udid" : "TEST-PHONE-UDID"
+      }
+    ],
+    "tvOS 11.4" : [
+      {
+        "state" : "Shutdown",
+        "availability" : "(available)",
+        "name" : "Apple TV",
+        "udid" : "TEST-TV-UDID"
+      }
+    ]
+  }
+}
+    ''';
+
+    MockProcessManager mockProcessManager;
+    SimControl simControl;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      when(mockProcessManager.runSync(any))
+          .thenReturn(ProcessResult(mockPid, 0, validSimControlOutput, ''));
+
+      simControl = SimControl();
+    });
+
+    testUsingContext('getDevices succeeds', () {
+      final List<SimDevice> devices = simControl.getDevices();
+
+      final SimDevice watch = devices[0];
+      expect(watch.category, 'watchOS 4.3');
+      expect(watch.state, 'Shutdown');
+      expect(watch.availability, '(available)');
+      expect(watch.name, 'Apple Watch - 38mm');
+      expect(watch.udid, 'TEST-WATCH-UDID');
+      expect(watch.isBooted, isFalse);
+
+      final SimDevice phone = devices[1];
+      expect(phone.category, 'iOS 11.4');
+      expect(phone.state, 'Booted');
+      expect(phone.availability, '(available)');
+      expect(phone.name, 'iPhone 5s');
+      expect(phone.udid, 'TEST-PHONE-UDID');
+      expect(phone.isBooted, isTrue);
+
+      final SimDevice tv = devices[2];
+      expect(tv.category, 'tvOS 11.4');
+      expect(tv.state, 'Shutdown');
+      expect(tv.availability, '(available)');
+      expect(tv.name, 'Apple TV');
+      expect(tv.udid, 'TEST-TV-UDID');
+      expect(tv.isBooted, isFalse);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+      SimControl: () => simControl,
+    });
+  });
+
+  group('startApp', () {
+    SimControl simControl;
+
+    setUp(() {
+      simControl = MockSimControl();
+    });
+
+    testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async {
+        final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
+        when(iosWorkflow.getPlistValueFromFile(any, any)).thenReturn('correct');
+
+        final Directory mockDir = fs.currentDirectory;
+        final IOSApp package = PrebuiltIOSApp(projectBundleId: 'incorrect', bundleName: 'name', bundleDir: mockDir);
+
+        const BuildInfo mockInfo = BuildInfo(BuildMode.debug, 'flavor');
+        final DebuggingOptions mockOptions = DebuggingOptions.disabled(mockInfo);
+        await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);
+
+        verify(simControl.launch(any, 'correct', any));
+      },
+      overrides: <Type, Generator>{
+        SimControl: () => simControl,
+        IOSWorkflow: () => MockIOSWorkflow()
+      },
+    );
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
+    fs.file('pubspec.yaml')
+      ..createSync()
+      ..writeAsStringSync(r'''
+name: example
+
+flutter:
+  module: {}
+''');
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSSimulator('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.directory('ios').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSSimulator('test').isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(IOSSimulator('test').isSupportedForProject(flutterProject), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/ios/xcode_backend_test.dart
new file mode 100644
index 0000000..692cced
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/xcode_backend_test.dart
@@ -0,0 +1,67 @@
+// Copyright 2018 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart';
+
+import '../../src/common.dart';
+
+const String xcodeBackendPath = 'bin/xcode_backend.sh';
+const String xcodeBackendErrorHeader = '========================================================================';
+
+// Acceptable $CONFIGURATION/$FLUTTER_BUILD_MODE values should be debug, profile, or release
+const Map<String, String> unknownConfiguration = <String, String>{
+  'CONFIGURATION': 'Custom',
+};
+
+// $FLUTTER_BUILD_MODE will override $CONFIGURATION
+const Map<String, String> unknownFlutterBuildMode = <String, String>{
+  'FLUTTER_BUILD_MODE': 'Custom',
+  'CONFIGURATION': 'Debug',
+};
+
+// Can't archive a non-release build.
+const Map<String, String> installWithoutRelease = <String, String>{
+  'CONFIGURATION': 'Debug',
+  'ACTION': 'install',
+};
+
+// Can't use a debug engine build with a release build.
+const Map<String, String> localEngineDebugBuildModeRelease = <String, String>{
+  'SOURCE_ROOT': '../../../examples/hello_world',
+  'FLUTTER_ROOT': '../../..',
+  'LOCAL_ENGINE': '/engine/src/out/ios_debug_unopt',
+  'CONFIGURATION': 'Release',
+};
+
+// Can't use a debug build with a profile engine.
+const Map<String, String> localEngineProfileBuildeModeRelease =
+    <String, String>{
+  'SOURCE_ROOT': '../../../examples/hello_world',
+  'FLUTTER_ROOT': '../../..',
+  'LOCAL_ENGINE': '/engine/src/out/ios_profile',
+  'CONFIGURATION': 'Debug',
+  'FLUTTER_BUILD_MODE': 'Debug',
+};
+
+void main() {
+  Future<void> expectXcodeBackendFails(Map<String, String> environment) async {
+    final ProcessResult result = await Process.run(
+      xcodeBackendPath,
+      <String>['build'],
+      environment: environment,
+    );
+    expect(result.stderr, startsWith(xcodeBackendErrorHeader));
+    expect(result.exitCode, isNot(0));
+  }
+
+  test('Xcode backend fails for on unsupported configuration combinations', () async {
+    await expectXcodeBackendFails(unknownConfiguration);
+    await expectXcodeBackendFails(unknownFlutterBuildMode);
+
+    await expectXcodeBackendFails(installWithoutRelease);
+
+    await expectXcodeBackendFails(localEngineDebugBuildModeRelease);
+    await expectXcodeBackendFails(localEngineProfileBuildeModeRelease);
+  }, skip: true); // #35707 non-hermetic test requires precache to have run.
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
new file mode 100644
index 0000000..9026ec0
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -0,0 +1,528 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/pubspec_schema.dart';
+
+const String xcodebuild = '/usr/bin/xcodebuild';
+
+void main() {
+  group('xcodebuild versioning', () {
+    MockProcessManager mockProcessManager;
+    XcodeProjectInterpreter xcodeProjectInterpreter;
+    FakePlatform macOS;
+    FileSystem fs;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      xcodeProjectInterpreter = XcodeProjectInterpreter();
+      macOS = fakePlatform('macos');
+      fs = MemoryFileSystem();
+      fs.file(xcodebuild).createSync(recursive: true);
+    });
+
+    void testUsingOsxContext(String description, dynamic testMethod()) {
+      testUsingContext(description, testMethod, overrides: <Type, Generator>{
+        ProcessManager: () => mockProcessManager,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+      });
+    }
+
+    testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
+      expect(xcodeProjectInterpreter.versionText, isNull);
+    });
+
+    testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
+        ProcessResult(
+          0,
+          1,
+          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
+          "but active developer directory '/Library/Developer/CommandLineTools' "
+          'is a command line tools instance',
+          '',
+        ),
+      );
+      expect(xcodeProjectInterpreter.versionText, isNull);
+    });
+
+    testUsingOsxContext('versionText returns formatted version text', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
+    });
+
+    testUsingOsxContext('versionText handles Xcode version string with unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
+    });
+
+    testUsingOsxContext('majorVersion returns major version', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.majorVersion, 8);
+    });
+
+    testUsingOsxContext('majorVersion is null when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.majorVersion, isNull);
+    });
+
+    testUsingOsxContext('minorVersion returns minor version', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, 3);
+    });
+
+    testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, 0);
+    });
+
+    testUsingOsxContext('minorVersion is null when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.minorVersion, isNull);
+    });
+
+    testUsingContext('isInstalled is false when not on MacOS', () {
+      fs.file(xcodebuild).deleteSync();
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    }, overrides: <Type, Generator>{
+      Platform: () => fakePlatform('notMacOS'),
+    });
+
+    testUsingOsxContext('isInstalled is false when xcodebuild does not exist', () {
+      fs.file(xcodebuild).deleteSync();
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is false when Xcode is not fully installed', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
+        ProcessResult(
+          0,
+          1,
+          "xcode-select: error: tool 'xcodebuild' requires Xcode, "
+          "but active developer directory '/Library/Developer/CommandLineTools' "
+          'is a command line tools instance',
+          '',
+        ),
+      );
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is false when version has unexpected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.isInstalled, isFalse);
+    });
+
+    testUsingOsxContext('isInstalled is true when version has expected format', () {
+      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
+          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
+      expect(xcodeProjectInterpreter.isInstalled, isTrue);
+    });
+  });
+  group('Xcode project properties', () {
+    test('properties from default project can be parsed', () {
+      const String output = '''
+Information about project "Runner":
+    Targets:
+        Runner
+
+    Build Configurations:
+        Debug
+        Release
+
+    If no build configuration is specified and -scheme is not passed then "Release" is used.
+
+    Schemes:
+        Runner
+
+''';
+      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
+      expect(info.targets, <String>['Runner']);
+      expect(info.schemes, <String>['Runner']);
+      expect(info.buildConfigurations, <String>['Debug', 'Release']);
+    });
+    test('properties from project with custom schemes can be parsed', () {
+      const String output = '''
+Information about project "Runner":
+    Targets:
+        Runner
+
+    Build Configurations:
+        Debug (Free)
+        Debug (Paid)
+        Release (Free)
+        Release (Paid)
+
+    If no build configuration is specified and -scheme is not passed then "Release (Free)" is used.
+
+    Schemes:
+        Free
+        Paid
+
+''';
+      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
+      expect(info.targets, <String>['Runner']);
+      expect(info.schemes, <String>['Free', 'Paid']);
+      expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
+    });
+    test('expected scheme for non-flavored build is Runner', () {
+      expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner');
+      expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner');
+      expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner');
+    });
+    test('expected build configuration for non-flavored build is derived from BuildMode', () {
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
+    });
+    test('expected scheme for flavored build is the title-cased flavor', () {
+      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello')), 'Hello');
+      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO')), 'HELLO');
+      expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello');
+    });
+    test('expected build configuration for flavored build is Mode-Flavor', () {
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello'), 'Hello'), 'Debug-Hello');
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Profile-Hello');
+      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello');
+    });
+    test('scheme for default project is Runner', () {
+      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
+      expect(info.schemeFor(BuildInfo.debug), 'Runner');
+      expect(info.schemeFor(BuildInfo.profile), 'Runner');
+      expect(info.schemeFor(BuildInfo.release), 'Runner');
+      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
+    });
+    test('build configuration for default project is matched against BuildMode', () {
+      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']);
+      expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
+      expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
+      expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
+    });
+    test('scheme for project with custom schemes is matched against flavor', () {
+      final XcodeProjectInfo info = XcodeProjectInfo(
+        <String>['Runner'],
+        <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
+        <String>['Free', 'Paid'],
+      );
+      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free')), 'Free');
+      expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free')), 'Free');
+      expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid')), 'Paid');
+      expect(info.schemeFor(const BuildInfo(BuildMode.debug, null)), isNull);
+      expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
+    });
+    test('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
+      final XcodeProjectInfo info = XcodeProjectInfo(
+        <String>['Runner'],
+        <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
+        <String>['Free', 'Paid'],
+      );
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free'), 'Free'), 'debug (free)');
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid'), 'Paid'), 'Debug paid');
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'profile - Free');
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid');
+    });
+    test('build configuration for project with inconsistent naming is null', () {
+      final XcodeProjectInfo info = XcodeProjectInfo(
+        <String>['Runner'],
+        <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
+        <String>['Free', 'Paid'],
+      );
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free'), 'Free'), null);
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null);
+      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null);
+    });
+  });
+
+  group('updateGeneratedXcodeProperties', () {
+    MockLocalEngineArtifacts mockArtifacts;
+    MockProcessManager mockProcessManager;
+    FakePlatform macOS;
+    FileSystem fs;
+
+    setUp(() {
+      fs = MemoryFileSystem();
+      mockArtifacts = MockLocalEngineArtifacts();
+      mockProcessManager = MockProcessManager();
+      macOS = fakePlatform('macos');
+      fs.file(xcodebuild).createSync(recursive: true);
+    });
+
+    void testUsingOsxContext(String description, dynamic testMethod()) {
+      testUsingContext(description, testMethod, overrides: <Type, Generator>{
+        Artifacts: () => mockArtifacts,
+        ProcessManager: () => mockProcessManager,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+      });
+    }
+
+    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
+
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
+      final FlutterProject project = FlutterProject.fromPath('path/to/project');
+      await updateGeneratedXcodeProperties(
+        project: project,
+        buildInfo: buildInfo,
+      );
+
+      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+      expect(config.existsSync(), isTrue);
+
+      final String contents = config.readAsStringSync();
+      expect(contents.contains('ARCHS=armv7'), isTrue);
+    });
+
+    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true);
+      final FlutterProject project = FlutterProject.fromPath('path/to/project');
+      await updateGeneratedXcodeProperties(
+        project: project,
+        buildInfo: buildInfo,
+      );
+
+      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+      expect(config.existsSync(), isTrue);
+
+      final String contents = config.readAsStringSync();
+      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
+    });
+
+    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
+      final FlutterProject project = FlutterProject.fromPath('path/to/project');
+      await updateGeneratedXcodeProperties(
+        project: project,
+        buildInfo: buildInfo,
+      );
+
+      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+      expect(config.existsSync(), isTrue);
+
+      final String contents = config.readAsStringSync();
+      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isFalse);
+    });
+
+    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
+      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null);
+
+      final FlutterProject project = FlutterProject.fromPath('path/to/project');
+      await updateGeneratedXcodeProperties(
+        project: project,
+        buildInfo: buildInfo,
+      );
+
+      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+      expect(config.existsSync(), isTrue);
+
+      final String contents = config.readAsStringSync();
+      expect(contents.contains('ARCHS=arm64'), isTrue);
+    });
+
+    String propertyFor(String key, File file) {
+      final List<String> properties = file
+          .readAsLinesSync()
+          .where((String line) => line.startsWith('$key='))
+          .map((String line) => line.split('=')[1])
+          .toList();
+      return properties.isEmpty ? null : properties.first;
+    }
+
+    Future<void> checkBuildVersion({
+      String manifestString,
+      BuildInfo buildInfo,
+      String expectedBuildName,
+      String expectedBuildNumber,
+    }) async {
+      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
+          platform: TargetPlatform.ios, mode: anyNamed('mode'))).thenReturn('engine');
+      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios'));
+
+      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
+      manifestFile.createSync(recursive: true);
+      manifestFile.writeAsStringSync(manifestString);
+
+      // write schemaData otherwise pubspec.yaml file can't be loaded
+      writeEmptySchemaFile(fs);
+
+      await updateGeneratedXcodeProperties(
+        project: FlutterProject.fromPath('path/to/project'),
+        buildInfo: buildInfo,
+      );
+
+      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
+      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
+      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
+    }
+
+    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingOsxContext('extract build name from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: null,
+      );
+    });
+
+    testUsingOsxContext('allow build info to override build name', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingOsxContext('allow build info to override build number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingOsxContext('allow build info to override build name and number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingOsxContext('allow build info to override build name and set number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingOsxContext('allow build info to set build name and number', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
+      await checkBuildVersion(
+        manifestString: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+  });
+}
+
+Platform fakePlatform(String name) {
+  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
+}
+
+class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter { }
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
new file mode 100644
index 0000000..29a5613
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart
@@ -0,0 +1,97 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/linux/application_package.dart';
+import 'package:flutter_tools/src/linux/linux_device.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(LinuxDevice, () {
+    final LinuxDevice device = LinuxDevice();
+    final MockPlatform notLinux = MockPlatform();
+    final MockProcessManager mockProcessManager = MockProcessManager();
+
+    when(notLinux.isLinux).thenReturn(false);
+    when(mockProcessManager.run(<String>[
+      'ps', 'aux',
+    ])).thenAnswer((Invocation invocation) async {
+      final MockProcessResult result = MockProcessResult();
+      when(result.exitCode).thenReturn(0);
+      when<String>(result.stdout).thenReturn('');
+      return result;
+    });
+
+    testUsingContext('defaults', () async {
+      final PrebuiltLinuxApp linuxApp = PrebuiltLinuxApp(executable: 'foo');
+      expect(await device.targetPlatform, TargetPlatform.linux_x64);
+      expect(device.name, 'Linux');
+      expect(await device.installApp(linuxApp), true);
+      expect(await device.uninstallApp(linuxApp), true);
+      expect(await device.isLatestBuildInstalled(linuxApp), true);
+      expect(await device.isAppInstalled(linuxApp), true);
+      expect(await device.stopApp(linuxApp), true);
+      expect(device.category, Category.desktop);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    test('noop port forwarding', () async {
+      final LinuxDevice device = LinuxDevice();
+      final DevicePortForwarder portForwarder = device.portForwarder;
+      final int result = await portForwarder.forward(2);
+      expect(result, 2);
+      expect(portForwarder.forwardedPorts.isEmpty, true);
+    });
+
+    testUsingContext('No devices listed if platform unsupported', () async {
+      expect(await LinuxDevices().devices, <Device>[]);
+    }, overrides: <Type, Generator>{
+      Platform: () => notLinux,
+    });
+  });
+
+  testUsingContext('LinuxDevice.isSupportedForProject is true with editable host app', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    fs.directory('linux').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(LinuxDevice().isSupportedForProject(flutterProject), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+
+  testUsingContext('LinuxDevice.isSupportedForProject is false with no host app', () async {
+    fs.file('pubspec.yaml').createSync();
+    fs.file('.packages').createSync();
+    final FlutterProject flutterProject = FlutterProject.current();
+
+    expect(LinuxDevice().isSupportedForProject(flutterProject), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem(),
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
+
+class MockFileSystem extends Mock implements FileSystem {}
+
+class MockFile extends Mock implements File {}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockProcess extends Mock implements Process {}
+
+class MockProcessResult extends Mock implements ProcessResult {}
diff --git a/packages/flutter_tools/test/general.shard/linux/linux_doctor_test.dart b/packages/flutter_tools/test/general.shard/linux/linux_doctor_test.dart
new file mode 100644
index 0000000..2302089
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/linux/linux_doctor_test.dart
@@ -0,0 +1,158 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/linux/linux_doctor.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group(LinuxDoctorValidator, () {
+    ProcessManager processManager;
+    LinuxDoctorValidator linuxDoctorValidator;
+
+    setUp(() {
+      processManager = MockProcessManager();
+      linuxDoctorValidator = LinuxDoctorValidator();
+    });
+
+    testUsingContext('Returns full validation when clang++ and make are availibe', () async {
+      when(processManager.run(<String>['clang++', '--version'])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'clang version 4.0.1-10 (tags/RELEASE_401/final)\njunk',
+          exitCode: 0,
+        );
+      });
+      when(processManager.run(<String>[
+        'make',
+        '--version',
+      ])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'GNU Make 4.1\njunk',
+          exitCode: 0,
+        );
+      });
+
+      final ValidationResult result = await linuxDoctorValidator.validate();
+      expect(result.type, ValidationType.installed);
+      expect(result.messages, <ValidationMessage>[
+        ValidationMessage('clang++ 4.0.1'),
+        ValidationMessage('GNU Make 4.1'),
+      ]);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('Returns partial validation when clang++ version is too old', () async {
+      when(processManager.run(<String>['clang++', '--version'])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'clang version 2.0.1-10 (tags/RELEASE_401/final)\njunk',
+          exitCode: 0,
+        );
+      });
+      when(processManager.run(<String>[
+        'make',
+        '--version',
+      ])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'GNU Make 4.1\njunk',
+          exitCode: 0,
+        );
+      });
+
+      final ValidationResult result = await linuxDoctorValidator.validate();
+      expect(result.type, ValidationType.partial);
+      expect(result.messages, <ValidationMessage>[
+        ValidationMessage.error('clang++ 2.0.1 is below minimum version of 3.4.0'),
+        ValidationMessage('GNU Make 4.1'),
+      ]);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('Returns mising validation when make is not availible', () async {
+      when(processManager.run(<String>['clang++', '--version'])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'clang version 4.0.1-10 (tags/RELEASE_401/final)\njunk',
+          exitCode: 0,
+        );
+      });
+      when(processManager.run(<String>[
+        'make',
+        '--version',
+      ])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: '',
+          exitCode: 1,
+        );
+      });
+
+      final ValidationResult result = await linuxDoctorValidator.validate();
+      expect(result.type, ValidationType.missing);
+      expect(result.messages, <ValidationMessage>[
+        ValidationMessage('clang++ 4.0.1'),
+        ValidationMessage.error('make is not installed')
+      ]);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+
+    testUsingContext('Returns mising validation when clang++ is not availible', () async {
+      when(processManager.run(<String>['clang++', '--version'])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: '',
+          exitCode: 1,
+        );
+      });
+      when(processManager.run(<String>[
+        'make',
+        '--version',
+      ])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: 'GNU Make 4.1\njunk',
+          exitCode: 0,
+        );
+      });
+
+      final ValidationResult result = await linuxDoctorValidator.validate();
+      expect(result.type, ValidationType.missing);
+      expect(result.messages, <ValidationMessage>[
+        ValidationMessage.error('clang++ is not installed'),
+        ValidationMessage('GNU Make 4.1'),
+      ]);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+
+
+    testUsingContext('Returns missing validation when clang and make are not availible', () async {
+      when(processManager.run(<String>['clang++', '--version'])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: '',
+          exitCode: 1,
+        );
+      });
+      when(processManager.run(<String>[
+        'make',
+        '--version',
+      ])).thenAnswer((_) async {
+        return FakeProcessResult(
+          stdout: '',
+          exitCode: 1,
+        );
+      });
+
+      final ValidationResult result = await linuxDoctorValidator.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => processManager,
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/linux/linux_workflow_test.dart b/packages/flutter_tools/test/general.shard/linux/linux_workflow_test.dart
new file mode 100644
index 0000000..1445cff
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/linux/linux_workflow_test.dart
@@ -0,0 +1,46 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:mockito/mockito.dart';
+import 'package:flutter_tools/src/linux/linux_workflow.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(LinuxWorkflow, () {
+    final MockPlatform linux = MockPlatform();
+    final MockPlatform linuxWithFde = MockPlatform()
+      ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
+    final MockPlatform notLinux = MockPlatform();
+    when(linux.isLinux).thenReturn(true);
+    when(linuxWithFde.isLinux).thenReturn(true);
+    when(notLinux.isLinux).thenReturn(false);
+
+    testUsingContext('Applies to linux platform', () {
+      expect(linuxWorkflow.appliesToHostPlatform, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => linux,
+    });
+    testUsingContext('Does not apply to non-linux platform', () {
+      expect(linuxWorkflow.appliesToHostPlatform, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => notLinux,
+    });
+
+    testUsingContext('defaults', () {
+      expect(linuxWorkflow.canListEmulators, false);
+      expect(linuxWorkflow.canLaunchDevices, true);
+      expect(linuxWorkflow.canListDevices, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => linuxWithFde,
+    });
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  final Map<String, String> environment = <String, String>{};
+}
diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
new file mode 100644
index 0000000..068b87a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
@@ -0,0 +1,588 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/cocoapods.dart';
+import 'package:flutter_tools/src/plugins.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+typedef InvokeProcess = Future<ProcessResult> Function();
+
+void main() {
+  FileSystem fs;
+  ProcessManager mockProcessManager;
+  MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+  FlutterProject projectUnderTest;
+  CocoaPods cocoaPodsUnderTest;
+  InvokeProcess resultOfPodVersion;
+
+  void pretendPodIsNotInstalled() {
+    resultOfPodVersion = () async => throw 'Executable does not exist';
+  }
+
+  void pretendPodVersionFails() {
+    resultOfPodVersion = () async => exitsWithError();
+  }
+
+  void pretendPodVersionIs(String versionText) {
+    resultOfPodVersion = () async => exitsHappy(versionText);
+  }
+
+  void podsIsInHomeDir() {
+    fs.directory(fs.path.join(homeDirPath, '.cocoapods', 'repos', 'master')).createSync(recursive: true);
+  }
+
+  String podsIsInCustomDir({String cocoapodsReposDir}) {
+    cocoapodsReposDir ??= fs.path.join(homeDirPath, 'cache', 'cocoapods', 'repos');
+    fs.directory(fs.path.join(cocoapodsReposDir, 'master')).createSync(recursive: true);
+    return cocoapodsReposDir;
+  }
+
+  setUp(() async {
+    Cache.flutterRoot = 'flutter';
+    fs = MemoryFileSystem();
+    mockProcessManager = MockProcessManager();
+    mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+    projectUnderTest = FlutterProject.fromDirectory(fs.directory('project'));
+    projectUnderTest.ios.xcodeProject.createSync(recursive: true);
+    cocoaPodsUnderTest = CocoaPods();
+    pretendPodVersionIs('1.6.0');
+    fs.file(fs.path.join(
+      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc',
+    ))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Objective-C iOS podfile template');
+    fs.file(fs.path.join(
+      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift',
+    ))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Swift iOS podfile template');
+    fs.file(fs.path.join(
+      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos',
+    ))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('macOS podfile template');
+    when(mockProcessManager.run(
+      <String>['pod', '--version'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenAnswer((_) => resultOfPodVersion());
+    when(mockProcessManager.run(
+      <String>['pod', 'install', '--verbose'],
+      workingDirectory: 'project/ios',
+      environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
+    )).thenAnswer((_) async => exitsHappy());
+    when(mockProcessManager.run(
+      <String>['pod', 'install', '--verbose'],
+      workingDirectory: 'project/macos',
+      environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
+    )).thenAnswer((_) async => exitsHappy());
+  });
+
+  group('Evaluate installation', () {
+    testUsingContext('detects not installed, if pod exec does not exist', () async {
+      pretendPodIsNotInstalled();
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects not installed, if pod version fails', () async {
+      pretendPodVersionFails();
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects installed', () async {
+      pretendPodVersionIs('0.0.1');
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects unknown version', () async {
+      pretendPodVersionIs('Plugin loaded.\n1.5.3');
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects below minimum version', () async {
+      pretendPodVersionIs('1.5.0');
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects at recommended version', () async {
+      pretendPodVersionIs('1.6.0');
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('detects above recommended version', () async {
+      pretendPodVersionIs('1.6.1');
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('Setup Podfile', () {
+    testUsingContext('creates objective-c Podfile when not present', () async {
+      cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
+
+      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('creates swift Podfile if swift', () async {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
+        'SWIFT_VERSION': '4.0',
+      });
+
+      final FlutterProject project = FlutterProject.fromPath('project');
+      cocoaPodsUnderTest.setupPodfile(project.ios);
+
+      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('creates macOS Podfile when not present', () async {
+      projectUnderTest.macos.xcodeProject.createSync(recursive: true);
+      cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
+
+      expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('does not recreate Podfile when already present', () async {
+      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
+
+      final FlutterProject project = FlutterProject.fromPath('project');
+      cocoaPodsUnderTest.setupPodfile(project.ios);
+
+      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+
+    testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+
+      final FlutterProject project = FlutterProject.fromPath('project');
+      cocoaPodsUnderTest.setupPodfile(project.ios);
+
+      expect(projectUnderTest.ios.podfile.existsSync(), false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('includes Pod config in xcconfig files, if not present', () async {
+      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.xcodeConfigFor('Debug')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing debug config');
+      projectUnderTest.ios.xcodeConfigFor('Release')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing release config');
+
+      final FlutterProject project = FlutterProject.fromPath('project');
+      cocoaPodsUnderTest.setupPodfile(project.ios);
+
+      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
+      expect(debugContents, contains(
+          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
+      expect(debugContents, contains('Existing debug config'));
+      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
+      expect(releaseContents, contains(
+          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
+      expect(releaseContents, contains('Existing release config'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+  });
+
+  group('Update xcconfig', () {
+    testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async {
+      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Custom Podfile');
+      projectUnderTest.ios.podfileLock..createSync()..writeAsStringSync('Podfile.lock from user executed `pod install`');
+      projectUnderTest.packagesFile..createSync()..writeAsStringSync('');
+      projectUnderTest.ios.xcodeConfigFor('Debug')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing debug config');
+      projectUnderTest.ios.xcodeConfigFor('Release')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing release config');
+
+      final FlutterProject project = FlutterProject.fromPath('project');
+      await injectPlugins(project);
+
+      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
+      expect(debugContents, contains(
+          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
+      expect(debugContents, contains('Existing debug config'));
+      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
+      expect(releaseContents, contains(
+          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
+      expect(releaseContents, contains('Existing release config'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+  });
+
+  group('Process pods', () {
+    setUp(() {
+      podsIsInHomeDir();
+    });
+
+    testUsingContext('prints error, if CocoaPods is not installed', () async {
+      pretendPodIsNotInstalled();
+      projectUnderTest.ios.podfile.createSync();
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+      );
+      verifyNever(mockProcessManager.run(
+      argThat(containsAllInOrder(<String>['pod', 'install'])),
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      ));
+      expect(testLogger.errorText, contains('not installed'));
+      expect(testLogger.errorText, contains('Skipping pod install'));
+      expect(didInstall, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('throws, if Podfile is missing.', () async {
+      try {
+        await cocoaPodsUnderTest.processPods(
+          xcodeProject: projectUnderTest.ios,
+          engineDir: 'engine/path',
+        );
+        fail('ToolExit expected');
+      } catch(e) {
+        expect(e, isInstanceOf<ToolExit>());
+        verifyNever(mockProcessManager.run(
+        argThat(containsAllInOrder(<String>['pod', 'install'])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ));
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('throws, if specs repo is outdated.', () async {
+      fs.file(fs.path.join('project', 'ios', 'Podfile'))
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+
+      when(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      )).thenAnswer((_) async => exitsWithError(
+        '''
+[!] Unable to satisfy the following requirements:
+
+- `Firebase/Auth` required by `Podfile`
+- `Firebase/Auth (= 4.0.0)` required by `Podfile.lock`
+
+None of your spec sources contain a spec satisfying the dependencies: `Firebase/Auth, Firebase/Auth (= 4.0.0)`.
+
+You have either:
+ * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
+ * mistyped the name or version.
+ * not added the source repo that hosts the Podspec to your Podfile.
+
+Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.''',
+      ));
+      try {
+        await cocoaPodsUnderTest.processPods(
+          xcodeProject: projectUnderTest.ios,
+          engineDir: 'engine/path',
+        );
+        fail('ToolExit expected');
+      } catch (e) {
+        expect(e, isInstanceOf<ToolExit>());
+        expect(
+          testLogger.errorText,
+          contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
+        );
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('run pod install, if Podfile.lock is missing', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing lock file.');
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: false,
+      );
+      expect(didInstall, isTrue);
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('runs pod install, if Manifest.lock is missing', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: false,
+      );
+      expect(didInstall, isTrue);
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Different lock file.');
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: false,
+      );
+      expect(didInstall, isTrue);
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('runs pod install, if flutter framework changed', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing lock file.');
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: true,
+      );
+      expect(didInstall, isTrue);
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing lock file.');
+      await Future<void>.delayed(const Duration(milliseconds: 10));
+      projectUnderTest.ios.podfile
+        ..writeAsStringSync('Updated Podfile');
+      await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: false,
+      );
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('skips pod install, if nothing changed', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing lock file.');
+      final bool didInstall = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+        dependenciesChanged: false,
+      );
+      expect(didInstall, isFalse);
+      verifyNever(mockProcessManager.run(
+      argThat(containsAllInOrder(<String>['pod', 'install'])),
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      ));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
+      projectUnderTest.ios.podfile
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+      projectUnderTest.ios.podfileLock
+        ..createSync()
+        ..writeAsStringSync('Existing lock file.');
+      projectUnderTest.ios.podManifestLock
+        ..createSync(recursive: true)
+        ..writeAsStringSync('Existing lock file.');
+
+      when(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{
+          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+          'COCOAPODS_DISABLE_STATS': 'true',
+        },
+      )).thenAnswer(
+        (_) async => exitsWithError()
+      );
+
+      try {
+        await cocoaPodsUnderTest.processPods(
+          xcodeProject: projectUnderTest.ios,
+          engineDir: 'engine/path',
+          dependenciesChanged: true,
+        );
+        fail('Tool throw expected when pod install fails');
+      } on ToolExit {
+        expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
+      }
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+
+  group('Pods repos dir is custom', () {
+    String cocoapodsRepoDir;
+    Map<String, String> environment;
+    setUp(() {
+      cocoapodsRepoDir = podsIsInCustomDir();
+      environment = <String, String>{
+        'FLUTTER_FRAMEWORK_DIR': 'engine/path',
+        'COCOAPODS_DISABLE_STATS': 'true',
+        'CP_REPOS_DIR': cocoapodsRepoDir,
+      };
+    });
+
+    testUsingContext('succeeds, if specs repo is in CP_REPOS_DIR.', () async {
+      fs.file(fs.path.join('project', 'ios', 'Podfile'))
+        ..createSync()
+        ..writeAsStringSync('Existing Podfile');
+
+      when(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: environment,
+      )).thenAnswer((_) async => exitsHappy());
+      final bool success = await cocoaPodsUnderTest.processPods(
+        xcodeProject: projectUnderTest.ios,
+        engineDir: 'engine/path',
+      );
+      expect(success, true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+      Platform: () => FakePlatform(environment: environment),
+    });
+  });
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
+
+ProcessResult exitsWithError([ String stdout = '' ]) => ProcessResult(1, 1, stdout, '');
+ProcessResult exitsHappy([ String stdout = '' ]) => ProcessResult(1, 0, stdout, '');
diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart
new file mode 100644
index 0000000..2dc8894
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart
@@ -0,0 +1,91 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/macos/cocoapods.dart';
+import 'package:flutter_tools/src/macos/cocoapods_validator.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group('CocoaPods validation', () {
+    MockCocoaPods cocoaPods;
+
+    setUp(() {
+      cocoaPods = MockCocoaPods();
+      when(cocoaPods.evaluateCocoaPodsInstallation)
+          .thenAnswer((_) async => CocoaPodsStatus.recommended);
+      when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => true);
+      when(cocoaPods.cocoaPodsVersionText).thenAnswer((_) async => '1.8.0');
+    });
+
+    testUsingContext('Emits installed status when CocoaPods is installed', () async {
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+
+    testUsingContext('Emits missing status when CocoaPods is not installed', () async {
+      when(cocoaPods.evaluateCocoaPodsInstallation)
+          .thenAnswer((_) async => CocoaPodsStatus.notInstalled);
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+
+    testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async {
+      when(cocoaPods.evaluateCocoaPodsInstallation)
+          .thenAnswer((_) async => CocoaPodsStatus.unknownVersion);
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+
+    testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
+      when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false);
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+
+    testUsingContext('Emits partial status when CocoaPods version is too low', () async {
+      when(cocoaPods.evaluateCocoaPodsInstallation)
+          .thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion);
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+
+    testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
+      final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false);
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      CocoaPods: () => cocoaPods,
+    });
+  });
+}
+
+class MockCocoaPods extends Mock implements CocoaPods {}
+
+class CocoaPodsTestTarget extends CocoaPodsValidator {
+  CocoaPodsTestTarget({
+    this.hasHomebrew = true,
+  });
+
+  @override
+  final bool hasHomebrew;
+}
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
new file mode 100644
index 0000000..2dcb59ed
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart
@@ -0,0 +1,148 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.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/macos/application_package.dart';
+import 'package:flutter_tools/src/macos/macos_device.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group(MacOSDevice, () {
+    final MockPlatform notMac = MockPlatform();
+    final MacOSDevice device = MacOSDevice();
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(notMac.isMacOS).thenReturn(false);
+    when(notMac.environment).thenReturn(const <String, String>{});
+    when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+      return ProcessResult(0, 1, '', '');
+    });
+
+    testUsingContext('defaults', () async {
+      final MockMacOSApp mockMacOSApp = MockMacOSApp();
+      when(mockMacOSApp.executable(any)).thenReturn('foo');
+      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(await device.stopApp(mockMacOSApp), false);
+      expect(device.category, Category.desktop);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('stopApp', () async {
+      const String psOut = r'''
+tester    17193   0.0  0.2  4791128  37820   ??  S     2:27PM   0:00.09 /Applications/foo
+''';
+      final MockMacOSApp mockMacOSApp = MockMacOSApp();
+      when(mockMacOSApp.executable(any)).thenReturn('/Applications/foo');
+      when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
+        return ProcessResult(1, 0, psOut, '');
+      });
+      when(mockProcessManager.run(<String>['kill', '17193'])).thenAnswer((Invocation invocation) async {
+        return ProcessResult(2, 0, '', '');
+      });
+      expect(await device.stopApp(mockMacOSApp), true);
+      verify(mockProcessManager.run(<String>['kill', '17193']));
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    group('startApp', () {
+      final MockMacOSApp macOSApp = MockMacOSApp();
+      final MockFileSystem mockFileSystem = MockFileSystem();
+      final MockProcessManager mockProcessManager = MockProcessManager();
+      final MockFile mockFile = MockFile();
+      when(macOSApp.executable(any)).thenReturn('test');
+      when(mockFileSystem.file('test')).thenReturn(mockFile);
+      when(mockFile.existsSync()).thenReturn(true);
+      when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
+        return FakeProcess(
+          exitCode: Completer<int>().future,
+          stdout: Stream<List<int>>.fromIterable(<List<int>>[
+            utf8.encode('Observatory listening on http://127.0.0.1/0\n'),
+          ]),
+          stderr: const Stream<List<int>>.empty(),
+        );
+      });
+      when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+        return ProcessResult(0, 1, '', '');
+      });
+
+      testUsingContext('Can run from prebuilt application', () async {
+        final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true);
+        expect(result.started, true);
+        expect(result.observatoryUri, Uri.parse('http://127.0.0.1/0'));
+      }, overrides: <Type, Generator>{
+        FileSystem: () => mockFileSystem,
+        ProcessManager: () => mockProcessManager,
+      });
+    });
+
+    test('noop port forwarding', () async {
+      final MacOSDevice device = MacOSDevice();
+      final DevicePortForwarder portForwarder = device.portForwarder;
+      final int result = await portForwarder.forward(2);
+      expect(result, 2);
+      expect(portForwarder.forwardedPorts.isEmpty, true);
+    });
+
+    testUsingContext('No devices listed if platform unsupported', () async {
+      expect(await MacOSDevices().devices, <Device>[]);
+    }, overrides: <Type, Generator>{
+      Platform: () => notMac,
+    });
+
+    testUsingContext('isSupportedForProject is true with editable host app', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      fs.directory('macos').createSync();
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(MacOSDevice().isSupportedForProject(flutterProject), true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
+
+    testUsingContext('isSupportedForProject is false with no host app', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(MacOSDevice().isSupportedForProject(flutterProject), false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
+
+class MockMacOSApp extends Mock implements MacOSApp {}
+
+class MockFileSystem extends Mock implements FileSystem {}
+
+class MockFile extends Mock implements File {}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockProcess extends Mock implements Process {}
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
new file mode 100644
index 0000000..b63bdde
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/macos_workflow_test.dart
@@ -0,0 +1,55 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/macos/macos_workflow.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(MacOSWorkflow, () {
+    final MockPlatform mac = MockPlatform();
+    final MockPlatform macWithFde = MockPlatform()
+      ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
+    final MockPlatform notMac = MockPlatform();
+    when(mac.isMacOS).thenReturn(true);
+    when(macWithFde.isMacOS).thenReturn(true);
+    when(notMac.isMacOS).thenReturn(false);
+
+    final MockProcessManager mockProcessManager = MockProcessManager();
+    when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
+      return ProcessResult(0, 1, '', '');
+    });
+    testUsingContext('Applies to mac platform', () {
+      expect(macOSWorkflow.appliesToHostPlatform, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => mac,
+    });
+    testUsingContext('Does not apply to non-mac platform', () {
+      expect(macOSWorkflow.appliesToHostPlatform, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => notMac,
+    });
+
+    testUsingContext('defaults', () {
+      expect(macOSWorkflow.canListEmulators, false);
+      expect(macOSWorkflow.canLaunchDevices, true);
+      expect(macOSWorkflow.canListDevices, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => macWithFde,
+    });
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{};
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
new file mode 100644
index 0000000..ece4fd7
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -0,0 +1,113 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
+
+void main() {
+  group('Xcode', () {
+    MockProcessManager mockProcessManager;
+    Xcode xcode;
+    MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+      xcode = Xcode();
+    });
+
+    testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () {
+      when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
+          .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
+      expect(xcode.xcodeSelectPath, isNull);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () {
+      const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
+      when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
+          .thenReturn(ProcessResult(1, 0, xcodePath, ''));
+      expect(xcode.xcodeSelectPath, xcodePath);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(8);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(17);
+      expect(xcode.isVersionSatisfactory, isFalse);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
+      expect(xcode.isVersionSatisfactory, isFalse);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+      expect(xcode.isVersionSatisfactory, isTrue);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
+      expect(xcode.isVersionSatisfactory, isTrue);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
+      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
+      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
+      when(mockXcodeProjectInterpreter.minorVersion).thenReturn(1);
+      expect(xcode.isVersionSatisfactory, isTrue);
+    }, overrides: <Type, Generator>{
+      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+    });
+
+    testUsingContext('eulaSigned is false when clang is not installed', () {
+      when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+          .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
+      expect(xcode.eulaSigned, isFalse);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
+      when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+          .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.'));
+      expect(xcode.eulaSigned, isFalse);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
+      when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
+          .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files'));
+      expect(xcode.eulaSigned, isTrue);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart
new file mode 100644
index 0000000..e461fac
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart
@@ -0,0 +1,105 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:flutter_tools/src/macos/xcode_validator.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockXcode extends Mock implements Xcode {}
+
+void main() {
+  group('Xcode validation', () {
+    MockXcode xcode;
+    MockProcessManager processManager;
+
+    setUp(() {
+      xcode = MockXcode();
+      processManager = MockProcessManager();
+    });
+
+    testUsingContext('Emits missing status when Xcode is not installed', () async {
+      when(xcode.isInstalled).thenReturn(false);
+      when(xcode.xcodeSelectPath).thenReturn(null);
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+    });
+
+    testUsingContext('Emits missing status when Xcode installation is incomplete', () async {
+      when(xcode.isInstalled).thenReturn(false);
+      when(xcode.xcodeSelectPath).thenReturn('/Library/Developer/CommandLineTools');
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+    });
+
+    testUsingContext('Emits partial status when Xcode version too low', () async {
+      when(xcode.isInstalled).thenReturn(true);
+      when(xcode.versionText)
+          .thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
+      when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
+      when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+    });
+
+    testUsingContext('Emits partial status when Xcode EULA not signed', () async {
+      when(xcode.isInstalled).thenReturn(true);
+      when(xcode.versionText)
+          .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+      when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+      when(xcode.eulaSigned).thenReturn(false);
+      when(xcode.isSimctlInstalled).thenReturn(true);
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+    });
+
+    testUsingContext('Emits partial status when simctl is not installed', () async {
+      when(xcode.isInstalled).thenReturn(true);
+      when(xcode.versionText)
+          .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+      when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+      when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(false);
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+    });
+
+
+    testUsingContext('Succeeds when all checks pass', () async {
+      when(xcode.isInstalled).thenReturn(true);
+      when(xcode.versionText)
+          .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+      when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+      when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
+      const XcodeValidator validator = XcodeValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      Xcode: () => xcode,
+      ProcessManager: () => processManager,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart
new file mode 100644
index 0000000..d25ef6d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/project_file_invalidator_test.dart
@@ -0,0 +1,35 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('ProjectFileInvalidator', () {
+    final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
+    testUsingContext('Empty project', () async {
+      expect(
+        ProjectFileInvalidator.findInvalidated(lastCompiled: DateTime.now(), urisToMonitor: <Uri>[], packagesPath: ''),
+        isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+    });
+
+    testUsingContext('Non-existent files are ignored', () async {
+      expect(
+        ProjectFileInvalidator.findInvalidated(
+            lastCompiled: DateTime.now(),
+            urisToMonitor: <Uri>[Uri.parse('/not-there-anymore'),],
+            packagesPath: '',
+          ),
+        isEmpty);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart
new file mode 100644
index 0000000..acc91e8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/project_test.dart
@@ -0,0 +1,653 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/flutter_manifest.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/testbed.dart';
+
+void main() {
+  group('Project', () {
+    group('construction', () {
+      testInMemory('fails on null directory', () async {
+        expect(
+          () => FlutterProject.fromDirectory(null),
+          throwsA(isInstanceOf<AssertionError>()),
+        );
+      });
+
+      testInMemory('fails on invalid pubspec.yaml', () async {
+        final Directory directory = fs.directory('myproject');
+        directory.childFile('pubspec.yaml')
+          ..createSync(recursive: true)
+          ..writeAsStringSync(invalidPubspec);
+
+        expect(
+          () => FlutterProject.fromDirectory(directory),
+          throwsA(isInstanceOf<ToolExit>()),
+        );
+      });
+
+      testInMemory('fails on pubspec.yaml parse failure', () async {
+        final Directory directory = fs.directory('myproject');
+        directory.childFile('pubspec.yaml')
+          ..createSync(recursive: true)
+          ..writeAsStringSync(parseErrorPubspec);
+
+        expect(
+          () => FlutterProject.fromDirectory(directory),
+          throwsA(isInstanceOf<ToolExit>()),
+        );
+      });
+
+      testInMemory('fails on invalid example/pubspec.yaml', () async {
+        final Directory directory = fs.directory('myproject');
+        directory.childDirectory('example').childFile('pubspec.yaml')
+          ..createSync(recursive: true)
+          ..writeAsStringSync(invalidPubspec);
+
+        expect(
+          () => FlutterProject.fromDirectory(directory),
+          throwsA(isInstanceOf<ToolExit>()),
+        );
+      });
+
+      testInMemory('treats missing pubspec.yaml as empty', () async {
+        final Directory directory = fs.directory('myproject')
+          ..createSync(recursive: true);
+        expect((FlutterProject.fromDirectory(directory)).manifest.isEmpty,
+          true,
+        );
+      });
+
+      testInMemory('reads valid pubspec.yaml', () async {
+        final Directory directory = fs.directory('myproject');
+        directory.childFile('pubspec.yaml')
+          ..createSync(recursive: true)
+          ..writeAsStringSync(validPubspec);
+        expect(
+          FlutterProject.fromDirectory(directory).manifest.appName,
+          'hello',
+        );
+      });
+
+      testInMemory('sets up location', () async {
+        final Directory directory = fs.directory('myproject');
+        expect(
+          FlutterProject.fromDirectory(directory).directory.absolute.path,
+          directory.absolute.path,
+        );
+        expect(
+          FlutterProject.fromPath(directory.path).directory.absolute.path,
+          directory.absolute.path,
+        );
+        expect(
+          FlutterProject.current().directory.absolute.path,
+          fs.currentDirectory.absolute.path,
+        );
+      });
+    });
+
+    group('editable Android host app', () {
+      testInMemory('fails on non-module', () async {
+        final FlutterProject project = await someProject();
+        await expectLater(
+          project.android.makeHostAppEditable(),
+          throwsA(isInstanceOf<AssertionError>()),
+        );
+      });
+      testInMemory('exits on already editable module', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.makeHostAppEditable();
+        return expectToolExitLater(project.android.makeHostAppEditable(), contains('already editable'));
+      });
+      testInMemory('creates android/app folder in place of .android/app', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.makeHostAppEditable();
+        expectNotExists(project.directory.childDirectory('.android').childDirectory('app'));
+        expect(
+          project.directory.childDirectory('.android').childFile('settings.gradle').readAsStringSync(),
+          isNot(contains("include ':app'")),
+        );
+        expectExists(project.directory.childDirectory('android').childDirectory('app'));
+        expectExists(project.directory.childDirectory('android').childFile('local.properties'));
+        expect(
+          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
+          contains("include ':app'"),
+        );
+      });
+      testInMemory('retains .android/Flutter folder and references it', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.makeHostAppEditable();
+        expectExists(project.directory.childDirectory('.android').childDirectory('Flutter'));
+        expect(
+          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
+          contains('new File(settingsDir.parentFile, \'.android/include_flutter.groovy\')'),
+        );
+      });
+      testInMemory('can be redone after deletion', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.makeHostAppEditable();
+        project.directory.childDirectory('android').deleteSync(recursive: true);
+        await project.android.makeHostAppEditable();
+        expectExists(project.directory.childDirectory('android').childDirectory('app'));
+      });
+    });
+
+    group('ensure ready for platform-specific tooling', () {
+      testInMemory('does nothing, if project is not created', () async {
+        final FlutterProject project = FlutterProject(
+          fs.directory('not_created'),
+          FlutterManifest.empty(),
+          FlutterManifest.empty(),
+        );
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectNotExists(project.directory);
+      });
+      testInMemory('does nothing in plugin or package root project', () async {
+        final FlutterProject project = await aPluginProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
+        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
+        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
+        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
+      });
+      testInMemory('injects plugins for iOS', () async {
+        final FlutterProject project = await someProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
+      });
+      testInMemory('generates Xcode configuration for iOS', () async {
+        final FlutterProject project = await someProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
+      });
+      testInMemory('injects plugins for Android', () async {
+        final FlutterProject project = await someProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
+      });
+      testInMemory('updates local properties for Android', () async {
+        final FlutterProject project = await someProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
+      });
+      testInMemory('creates Android library in module', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
+        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
+        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
+      });
+      testInMemory('creates iOS pod in module', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.ensureReadyForPlatformSpecificTooling();
+        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
+        expectExists(flutter.childFile('podhelper.rb'));
+        expectExists(flutter.childFile('Generated.xcconfig'));
+        final Directory pluginRegistrantClasses = flutter
+            .childDirectory('FlutterPluginRegistrant')
+            .childDirectory('Classes');
+        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
+        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
+      });
+    });
+
+    group('module status', () {
+      testInMemory('is known for module', () async {
+        final FlutterProject project = await aModuleProject();
+        expect(project.isModule, isTrue);
+        expect(project.android.isModule, isTrue);
+        expect(project.ios.isModule, isTrue);
+        expect(project.android.hostAppGradleRoot.basename, '.android');
+        expect(project.ios.hostAppRoot.basename, '.ios');
+      });
+      testInMemory('is known for non-module', () async {
+        final FlutterProject project = await someProject();
+        expect(project.isModule, isFalse);
+        expect(project.android.isModule, isFalse);
+        expect(project.ios.isModule, isFalse);
+        expect(project.android.hostAppGradleRoot.basename, 'android');
+        expect(project.ios.hostAppRoot.basename, 'ios');
+      });
+    });
+
+    group('example', () {
+      testInMemory('exists for plugin', () async {
+        final FlutterProject project = await aPluginProject();
+        expect(project.hasExampleApp, isTrue);
+      });
+      testInMemory('does not exist for non-plugin', () async {
+        final FlutterProject project = await someProject();
+        expect(project.hasExampleApp, isFalse);
+      });
+    });
+
+    group('language', () {
+      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+      MemoryFileSystem fs;
+      setUp(() {
+        fs = MemoryFileSystem();
+        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+      });
+
+      testInMemory('default host app language', () async {
+        final FlutterProject project = await someProject();
+        expect(project.ios.isSwift, isFalse);
+        expect(project.android.isKotlin, isFalse);
+      });
+
+      testUsingContext('swift and kotlin host app language', () async {
+        final FlutterProject project = await someProject();
+
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
+          'SWIFT_VERSION': '4.0',
+        });
+        addAndroidGradleFile(project.directory,
+          gradleFileContent: () {
+      return '''
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+''';
+        });
+        expect(project.ios.isSwift, isTrue);
+        expect(project.android.isKotlin, isTrue);
+      }, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+      });
+    });
+
+    group('product bundle identifier', () {
+      MemoryFileSystem fs;
+      MockIOSWorkflow mockIOSWorkflow;
+      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+      setUp(() {
+        fs = MemoryFileSystem();
+        mockIOSWorkflow = MockIOSWorkflow();
+        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
+      });
+
+      void testWithMocks(String description, Future<void> testMethod()) {
+        testUsingContext(description, testMethod, overrides: <Type, Generator>{
+          FileSystem: () => fs,
+          IOSWorkflow: () => mockIOSWorkflow,
+          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+        });
+      }
+
+      testWithMocks('null, if no pbxproj or plist entries', () async {
+        final FlutterProject project = await someProject();
+        expect(project.ios.productBundleIdentifier, isNull);
+      });
+      testWithMocks('from pbxproj file, if no plist', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject');
+        });
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+      });
+      testWithMocks('from plist, if no variables', () async {
+        final FlutterProject project = await someProject();
+        when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('io.flutter.someProject');
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+      });
+      testWithMocks('from pbxproj and plist, if default variable', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject');
+        });
+        when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+      });
+      testWithMocks('from pbxproj and plist, by substitution', () async {
+        final FlutterProject project = await someProject();
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
+          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
+          'SUFFIX': 'suffix',
+        });
+        when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER).\$(SUFFIX)');
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject.suffix');
+      });
+      testWithMocks('empty surrounded by quotes', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('', qualifier: '"');
+        });
+        expect(project.ios.productBundleIdentifier, '');
+      });
+      testWithMocks('surrounded by double quotes', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
+        });
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+      });
+      testWithMocks('surrounded by single quotes', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
+        });
+        expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+      });
+    });
+
+    group('organization names set', () {
+      testInMemory('is empty, if project not created', () async {
+        final FlutterProject project = await someProject();
+        expect(project.organizationNames, isEmpty);
+      });
+      testInMemory('is empty, if no platform folders exist', () async {
+        final FlutterProject project = await someProject();
+        project.directory.createSync();
+        expect(project.organizationNames, isEmpty);
+      });
+      testInMemory('is populated from iOS bundle identifier', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
+        });
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is populated from Android application ID', () async {
+        final FlutterProject project = await someProject();
+        addAndroidGradleFile(project.directory,
+          gradleFileContent: () {
+            return gradleFileWithApplicationId('io.flutter.someproject');
+          });
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is populated from iOS bundle identifier in plugin example', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.example.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
+        });
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is populated from Android application ID in plugin example', () async {
+        final FlutterProject project = await someProject();
+        addAndroidGradleFile(project.example.directory,
+          gradleFileContent: () {
+            return gradleFileWithApplicationId('io.flutter.someproject');
+          });
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is populated from Android group in plugin', () async {
+        final FlutterProject project = await someProject();
+        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is singleton, if sources agree', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject');
+        });
+        addAndroidGradleFile(project.directory,
+          gradleFileContent: () {
+            return gradleFileWithApplicationId('io.flutter.someproject');
+          });
+        expect(project.organizationNames, <String>['io.flutter']);
+      });
+      testInMemory('is non-singleton, if sources disagree', () async {
+        final FlutterProject project = await someProject();
+        addIosProjectFile(project.directory, projectFileContent: () {
+          return projectFileWithBundleId('io.flutter.someProject');
+        });
+        addAndroidGradleFile(project.directory,
+          gradleFileContent: () {
+            return gradleFileWithApplicationId('io.clutter.someproject');
+          });
+        expect(
+          project.organizationNames,
+          <String>['io.flutter', 'io.clutter'],
+        );
+      });
+    });
+  });
+
+  group('Regression test for invalid pubspec', () {
+    Testbed testbed;
+
+    setUp(() {
+      testbed = Testbed();
+    });
+
+    test('Handles asking for builders from an invalid pubspec', () => testbed.run(() {
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+# Hello, World
+''');
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(flutterProject.builders, null);
+    }));
+
+    test('Handles asking for builders from a trivial pubspec', () => testbed.run(() {
+      fs.file('pubspec.yaml')
+        ..createSync()
+        ..writeAsStringSync(r'''
+# Hello, World
+name: foo_bar
+''');
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(flutterProject.builders, null);
+    }));
+  });
+}
+
+Future<FlutterProject> someProject() async {
+  final Directory directory = fs.directory('some_project');
+  directory.childFile('.packages').createSync(recursive: true);
+  directory.childDirectory('ios').createSync(recursive: true);
+  directory.childDirectory('android').createSync(recursive: true);
+  return FlutterProject.fromDirectory(directory);
+}
+
+Future<FlutterProject> aPluginProject() async {
+  final Directory directory = fs.directory('plugin_project');
+  directory.childDirectory('ios').createSync(recursive: true);
+  directory.childDirectory('android').createSync(recursive: true);
+  directory.childDirectory('example').createSync(recursive: true);
+  directory.childFile('pubspec.yaml').writeAsStringSync('''
+name: my_plugin
+flutter:
+  plugin:
+    androidPackage: com.example
+    pluginClass: MyPlugin
+    iosPrefix: FLT
+''');
+  return FlutterProject.fromDirectory(directory);
+}
+
+Future<FlutterProject> aModuleProject() async {
+  final Directory directory = fs.directory('module_project');
+  directory.childFile('.packages').createSync(recursive: true);
+  directory.childFile('pubspec.yaml').writeAsStringSync('''
+name: my_module
+flutter:
+  module:
+    androidPackage: com.example
+''');
+  return FlutterProject.fromDirectory(directory);
+}
+
+/// Executes the [testMethod] in a context where the file system
+/// is in memory.
+@isTest
+void testInMemory(String description, Future<void> testMethod()) {
+  Cache.flutterRoot = getFlutterRoot();
+  final FileSystem testFileSystem = MemoryFileSystem(
+    style: platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
+  );
+  // Transfer needed parts of the Flutter installation folder
+  // to the in-memory file system used during testing.
+  transfer(Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem);
+  transfer(fs.directory(Cache.flutterRoot)
+      .childDirectory('packages')
+      .childDirectory('flutter_tools')
+      .childDirectory('templates'), testFileSystem);
+  transfer(fs.directory(Cache.flutterRoot)
+      .childDirectory('packages')
+      .childDirectory('flutter_tools')
+      .childDirectory('schema'), testFileSystem);
+  testUsingContext(
+    description,
+    testMethod,
+    overrides: <Type, Generator>{
+      FileSystem: () => testFileSystem,
+      Cache: () => Cache(),
+    },
+  );
+}
+
+/// Transfers files and folders from the local file system's Flutter
+/// installation to an (in-memory) file system used for testing.
+void transfer(FileSystemEntity entity, FileSystem target) {
+  if (entity is Directory) {
+    target.directory(entity.absolute.path).createSync(recursive: true);
+    for (FileSystemEntity child in entity.listSync()) {
+      transfer(child, target);
+    }
+  } else if (entity is File) {
+    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
+  } else {
+    throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
+  }
+}
+
+Future<void> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
+  try {
+    await future;
+    fail('ToolExit expected, but nothing thrown');
+  } on ToolExit catch(e) {
+    expect(e.message, messageMatcher);
+  } catch(e, trace) {
+    fail('ToolExit expected, got $e\n$trace');
+  }
+}
+
+void expectExists(FileSystemEntity entity) {
+  expect(entity.existsSync(), isTrue);
+}
+
+void expectNotExists(FileSystemEntity entity) {
+  expect(entity.existsSync(), isFalse);
+}
+
+void addIosProjectFile(Directory directory, {String projectFileContent()}) {
+  directory
+      .childDirectory('ios')
+      .childDirectory('Runner.xcodeproj')
+      .childFile('project.pbxproj')
+        ..createSync(recursive: true)
+    ..writeAsStringSync(projectFileContent());
+}
+
+void addAndroidGradleFile(Directory directory, { String gradleFileContent() }) {
+  directory
+      .childDirectory('android')
+      .childDirectory('app')
+      .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync(gradleFileContent());
+}
+
+void addAndroidWithGroup(Directory directory, String id) {
+  directory.childDirectory('android').childFile('build.gradle')
+    ..createSync(recursive: true)
+    ..writeAsStringSync(gradleFileWithGroupId(id));
+}
+
+String get validPubspec => '''
+name: hello
+flutter:
+''';
+
+String get invalidPubspec => '''
+name: hello
+flutter:
+  invalid:
+''';
+
+String get parseErrorPubspec => '''
+name: hello
+# Whitespace is important.
+flutter:
+    something:
+  something_else:
+''';
+
+String projectFileWithBundleId(String id, {String qualifier}) {
+  return '''
+97C147061CF9000F007C117D /* Debug */ = {
+  isa = XCBuildConfiguration;
+  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+  buildSettings = {
+    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
+    PRODUCT_NAME = "\$(TARGET_NAME)";
+  };
+  name = Debug;
+};
+''';
+}
+
+String gradleFileWithApplicationId(String id) {
+  return '''
+apply plugin: 'com.android.application'
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        applicationId '$id'
+    }
+}
+''';
+}
+
+String gradleFileWithGroupId(String id) {
+  return '''
+group '$id'
+version '1.0-SNAPSHOT'
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+}
+''';
+}
+
+File androidPluginRegistrant(Directory parent) {
+  return parent.childDirectory('src')
+    .childDirectory('main')
+    .childDirectory('java')
+    .childDirectory('io')
+    .childDirectory('flutter')
+    .childDirectory('plugins')
+    .childFile('GeneratedPluginRegistrant.java');
+}
+
+class MockIOSWorkflow extends Mock implements IOSWorkflow {}
+
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
+  @override
+  bool get isInstalled => true;
+}
diff --git a/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart b/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart
new file mode 100644
index 0000000..6ad073f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart
@@ -0,0 +1,245 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/protocol_discovery.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  group('service_protocol discovery', () {
+    MockDeviceLogReader logReader;
+    ProtocolDiscovery discoverer;
+
+    group('no port forwarding', () {
+      /// Performs test set-up functionality that must be performed as part of
+      /// the `test()` pass and not part of the `setUp()` pass.
+      ///
+      /// This exists to make sure we're not creating an error that tries to
+      /// cross an error-zone boundary. Our use of `testUsingContext()` runs the
+      /// test code inside an error zone, but the `setUp()` code is not run in
+      /// any zone. This creates the potential for errors that try to cross
+      /// error-zone boundaries, which are considered uncaught.
+      ///
+      /// This also exists for cases where our initialization requires access to
+      /// a `Context` object, which is only set up inside the zone.
+      ///
+      /// These issues do not pertain to real code and are a test-only concern,
+      /// because in real code, the zone is set up in `main()`.
+      ///
+      /// See also: [runZoned]
+      void initialize() {
+        logReader = MockDeviceLogReader();
+        discoverer = ProtocolDiscovery.observatory(logReader);
+      }
+
+      tearDown(() {
+        discoverer.cancel();
+        logReader.dispose();
+      });
+
+      testUsingContext('returns non-null uri future', () async {
+        initialize();
+        expect(discoverer.uri, isNotNull);
+      });
+
+      testUsingContext('discovers uri if logs already produced output', () async {
+        initialize();
+        logReader.addLine('HELLO WORLD');
+        logReader.addLine('Observatory listening on http://127.0.0.1:9999');
+        final Uri uri = await discoverer.uri;
+        expect(uri.port, 9999);
+        expect('$uri', 'http://127.0.0.1:9999');
+      });
+
+      testUsingContext('discovers uri if logs not yet produced output', () async {
+        initialize();
+        final Future<Uri> uriFuture = discoverer.uri;
+        logReader.addLine('Observatory listening on http://127.0.0.1:3333');
+        final Uri uri = await uriFuture;
+        expect(uri.port, 3333);
+        expect('$uri', 'http://127.0.0.1:3333');
+      });
+
+      testUsingContext('discovers uri with Ascii Esc code', () async {
+        initialize();
+        logReader.addLine('Observatory listening on http://127.0.0.1:3333\x1b[');
+        final Uri uri = await discoverer.uri;
+        expect(uri.port, 3333);
+        expect('$uri', 'http://127.0.0.1:3333');
+      });
+
+      testUsingContext('uri throws if logs produce bad line', () async {
+        initialize();
+        Timer.run(() {
+          logReader.addLine('Observatory listening on http://127.0.0.1:apple');
+        });
+        expect(discoverer.uri, throwsA(isFormatException));
+      });
+
+      testUsingContext('uri waits for correct log line', () async {
+        initialize();
+        final Future<Uri> uriFuture = discoverer.uri;
+        logReader.addLine('Observatory not listening...');
+        final Uri timeoutUri = Uri.parse('http://timeout');
+        final Uri actualUri = await uriFuture.timeout(
+          const Duration(milliseconds: 100),
+          onTimeout: () => timeoutUri,
+        );
+        expect(actualUri, timeoutUri);
+      });
+
+      testUsingContext('discovers uri if log line contains Android prefix', () async {
+        initialize();
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:52584');
+        final Uri uri = await discoverer.uri;
+        expect(uri.port, 52584);
+        expect('$uri', 'http://127.0.0.1:52584');
+      });
+
+      testUsingContext('discovers uri if log line contains auth key', () async {
+        initialize();
+        final Future<Uri> uriFuture = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await uriFuture;
+        expect(uri.port, 54804);
+        expect('$uri', 'http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+      });
+
+      testUsingContext('discovers uri if log line contains non-localhost', () async {
+        initialize();
+        final Future<Uri> uriFuture = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await uriFuture;
+        expect(uri.port, 54804);
+        expect('$uri', 'http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+      });
+    });
+
+    group('port forwarding', () {
+      testUsingContext('default port', () async {
+        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: MockPortForwarder(99),
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await nextUri;
+        expect(uri.port, 99);
+        expect('$uri', 'http://127.0.0.1:99/PTwjm8Ii8qg=/');
+
+        await discoverer.cancel();
+        logReader.dispose();
+      });
+
+      testUsingContext('specified port', () async {
+        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: MockPortForwarder(99),
+          hostPort: 1243,
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await nextUri;
+        expect(uri.port, 1243);
+        expect('$uri', 'http://127.0.0.1:1243/PTwjm8Ii8qg=/');
+
+        await discoverer.cancel();
+        logReader.dispose();
+      });
+
+      testUsingContext('specified port zero', () async {
+        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: MockPortForwarder(99),
+          hostPort: 0,
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await nextUri;
+        expect(uri.port, 99);
+        expect('$uri', 'http://127.0.0.1:99/PTwjm8Ii8qg=/');
+
+        await discoverer.cancel();
+        logReader.dispose();
+      });
+
+      testUsingContext('ipv6', () async {
+        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: MockPortForwarder(99),
+          hostPort: 54777,
+          ipv6: true,
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://127.0.0.1:54804/PTwjm8Ii8qg=/');
+        final Uri uri = await nextUri;
+        expect(uri.port, 54777);
+        expect('$uri', 'http://[::1]:54777/PTwjm8Ii8qg=/');
+
+        await discoverer.cancel();
+        logReader.dispose();
+      });
+
+      testUsingContext('ipv6 with Ascii Escape code', () async {
+        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
+          logReader,
+          portForwarder: MockPortForwarder(99),
+          hostPort: 54777,
+          ipv6: true,
+        );
+
+        // Get next port future.
+        final Future<Uri> nextUri = discoverer.uri;
+        logReader.addLine('I/flutter : Observatory listening on http://[::1]:54777/PTwjm8Ii8qg=/\x1b[');
+        final Uri uri = await nextUri;
+        expect(uri.port, 54777);
+        expect('$uri', 'http://[::1]:54777/PTwjm8Ii8qg=/');
+
+        await discoverer.cancel();
+        logReader.dispose();
+      });
+    });
+  });
+}
+
+class MockPortForwarder extends DevicePortForwarder {
+  MockPortForwarder([this.availablePort]);
+
+  final int availablePort;
+
+  @override
+  Future<int> forward(int devicePort, { int hostPort }) async {
+    hostPort ??= 0;
+    if (hostPort == 0) {
+      return availablePort;
+    }
+    return hostPort;
+  }
+
+  @override
+  List<ForwardedPort> get forwardedPorts => throw 'not implemented';
+
+  @override
+  Future<void> unforward(ForwardedPort forwardedPort) {
+    throw 'not implemented';
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
new file mode 100644
index 0000000..26b8112
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -0,0 +1,116 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/devfs.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/run_hot.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/testbed.dart';
+
+void main() {
+  group('ResidentRunner', () {
+    final Uri testUri = Uri.parse('foo://bar');
+    Testbed testbed;
+    MockDevice mockDevice;
+    MockVMService mockVMService;
+    MockDevFS mockDevFS;
+    ResidentRunner residentRunner;
+
+    setUp(() {
+      testbed = Testbed(setup: () {
+        residentRunner = HotRunner(
+          <FlutterDevice>[
+            mockDevice,
+          ],
+          stayResident: false,
+          debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+        );
+      });
+      mockDevice = MockDevice();
+      mockVMService = MockVMService();
+      mockDevFS = MockDevFS();
+      // DevFS Mocks
+      when(mockDevFS.lastCompiled).thenReturn(DateTime(2000));
+      when(mockDevFS.sources).thenReturn(<Uri>[]);
+      when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { });
+      // FlutterDevice Mocks.
+      when(mockDevice.updateDevFS(
+        // Intentionally provide empty list to match above mock.
+        invalidatedFiles: <Uri>[],
+        mainPath: anyNamed('mainPath'),
+        target: anyNamed('target'),
+        bundle: anyNamed('bundle'),
+        firstBuildTime: anyNamed('firstBuildTime'),
+        bundleFirstUpload: anyNamed('bundleFirstUpload'),
+        bundleDirty: anyNamed('bundleDirty'),
+        fullRestart: anyNamed('fullRestart'),
+        projectRootPath: anyNamed('projectRootPath'),
+        pathToReload: anyNamed('pathToReload'),
+      )).thenAnswer((Invocation invocation) async {
+        return UpdateFSReport(
+          success: true,
+          syncedBytes: 0,
+          invalidatedSourcesCount: 0,
+        );
+      });
+      when(mockDevice.devFS).thenReturn(mockDevFS);
+      when(mockDevice.views).thenReturn(<FlutterView>[
+        MockFlutterView(),
+      ]);
+      when(mockDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { });
+      when(mockDevice.observatoryUris).thenReturn(<Uri>[
+        testUri,
+      ]);
+      when(mockDevice.connect(
+        reloadSources: anyNamed('reloadSources'),
+        restart: anyNamed('restart'),
+        compileExpression: anyNamed('compileExpression')
+      )).thenAnswer((Invocation invocation) async { });
+      when(mockDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath')))
+        .thenAnswer((Invocation invocation) async {
+          return testUri;
+        });
+      when(mockDevice.vmServices).thenReturn(<VMService>[
+        mockVMService,
+      ]);
+      when(mockDevice.refreshViews()).thenAnswer((Invocation invocation) async { });
+      // VMService mocks.
+      when(mockVMService.wsAddress).thenReturn(testUri);
+      when(mockVMService.done).thenAnswer((Invocation invocation) {
+        final Completer<void> result = Completer<void>.sync();
+        return result.future;
+      });
+    });
+
+    test('Can attach to device successfully', () => testbed.run(() async {
+      final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
+      final Completer<void> onAppStart = Completer<void>.sync();
+      final Future<int> result = residentRunner.attach(
+        appStartedCompleter: onAppStart,
+        connectionInfoCompleter: onConnectionInfo,
+      );
+      final Future<DebugConnectionInfo> connectionInfo = onConnectionInfo.future;
+
+      expect(await result, 0);
+
+      verify(mockDevice.initLogReader()).called(1);
+
+      expect(onConnectionInfo.isCompleted, true);
+      expect((await connectionInfo).baseUri, 'foo://bar');
+      expect(onAppStart.isCompleted, true);
+    }));
+  });
+}
+
+class MockDevice extends Mock implements FlutterDevice {}
+class MockFlutterView extends Mock implements FlutterView {}
+class MockVMService extends Mock implements VMService {}
+class MockDevFS extends Mock implements DevFS {}
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart
new file mode 100644
index 0000000..3680746
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_runner_test.dart
@@ -0,0 +1,258 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+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/terminal.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import 'utils.dart';
+
+const String _kFlutterRoot = '/flutter/flutter';
+const String _kEngineRoot = '/flutter/engine';
+const String _kArbitraryEngineRoot = '/arbitrary/engine';
+const String _kProjectRoot = '/project';
+const String _kDotPackages = '.packages';
+
+void main() {
+  group('FlutterCommandRunner', () {
+    MemoryFileSystem fs;
+    Platform platform;
+    FlutterCommandRunner runner;
+    ProcessManager processManager;
+
+    setUpAll(() {
+      Cache.disableLocking();
+    });
+
+    setUp(() {
+      fs = MemoryFileSystem();
+      fs.directory(_kFlutterRoot).createSync(recursive: true);
+      fs.directory(_kProjectRoot).createSync(recursive: true);
+      fs.currentDirectory = _kProjectRoot;
+
+      platform = FakePlatform(
+        environment: <String, String>{
+          'FLUTTER_ROOT': _kFlutterRoot,
+        },
+        version: '1 2 3 4 5',
+      );
+
+      runner = createTestCommandRunner(DummyFlutterCommand());
+      processManager = MockProcessManager();
+    });
+
+    group('run', () {
+      testUsingContext('checks that Flutter installation is up-to-date', () async {
+        final MockFlutterVersion version = FlutterVersion.instance;
+        bool versionChecked = false;
+        when(version.checkFlutterVersionFreshness()).thenAnswer((_) async {
+          versionChecked = true;
+        });
+
+        await runner.run(<String>['dummy']);
+
+        expect(versionChecked, isTrue);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('works if --local-engine is specified and --local-engine-src-path is determined by sky_engine', () async {
+        fs.directory('$_kArbitraryEngineRoot/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/').createSync(recursive: true);
+        fs.directory('$_kArbitraryEngineRoot/src/out/host_debug').createSync(recursive: true);
+        fs.file(_kDotPackages).writeAsStringSync('sky_engine:file://$_kArbitraryEngineRoot/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/');
+        await runner.run(<String>['dummy', '--local-engine=ios_debug']);
+
+        // Verify that this also works if the sky_engine path is a symlink to the engine root.
+        fs.link('/symlink').createSync('$_kArbitraryEngineRoot');
+        fs.file(_kDotPackages).writeAsStringSync('sky_engine:file:///symlink/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/');
+        await runner.run(<String>['dummy', '--local-engine=ios_debug']);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('works if --local-engine is specified and --local-engine-src-path is specified', () async {
+        fs.directory('$_kArbitraryEngineRoot/src/out/ios_debug').createSync(recursive: true);
+        fs.directory('$_kArbitraryEngineRoot/src/out/host_debug').createSync(recursive: true);
+        await runner.run(<String>['dummy', '--local-engine-src-path=$_kArbitraryEngineRoot/src', '--local-engine=ios_debug']);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('works if --local-engine is specified and --local-engine-src-path is determined by flutter root', () async {
+        fs.directory('$_kEngineRoot/src/out/ios_debug').createSync(recursive: true);
+        fs.directory('$_kEngineRoot/src/out/host_debug').createSync(recursive: true);
+        await runner.run(<String>['dummy', '--local-engine=ios_debug']);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+      }, initializeFlutterRoot: false);
+    });
+
+    testUsingContext('Doesnt crash on invalid .packages file', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages')
+        ..createSync()
+        ..writeAsStringSync('Not a valid package');
+
+      await runner.run(<String>['dummy']);
+
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      Platform: () => platform,
+    }, initializeFlutterRoot: false);
+
+    group('version', () {
+      testUsingContext('checks that Flutter toJson output reports the flutter framework version', () async {
+        final ProcessResult result = ProcessResult(0, 0, 'random', '0');
+
+        when(processManager.runSync('git log -n 1 --pretty=format:%H'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git rev-parse --abbrev-ref --symbolic @{u}'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git rev-parse --abbrev-ref HEAD'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git ls-remote --get-url master'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git log -n 1 --pretty=format:%ar'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git describe --match v*.*.* --first-parent --long --tags'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+        when(processManager.runSync('git log -n 1 --pretty=format:%ad --date=iso'.split(' '),
+          workingDirectory: Cache.flutterRoot)).thenReturn(result);
+
+        final FakeFlutterVersion version = FakeFlutterVersion();
+
+        // Because the hash depends on the time, we just use the 0.0.0-unknown here.
+        expect(version.toJson()['frameworkVersion'], '0.10.3');
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+        ProcessManager: () => processManager,
+      }, initializeFlutterRoot: false);
+    });
+
+    group('getRepoPackages', () {
+      setUp(() {
+        fs.directory(fs.path.join(_kFlutterRoot, 'examples'))
+            .createSync(recursive: true);
+        fs.directory(fs.path.join(_kFlutterRoot, 'packages'))
+            .createSync(recursive: true);
+        fs.directory(fs.path.join(_kFlutterRoot, 'dev', 'tools', 'aatool'))
+            .createSync(recursive: true);
+
+        fs.file(fs.path.join(_kFlutterRoot, 'dev', 'tools', 'pubspec.yaml'))
+            .createSync();
+        fs.file(fs.path.join(_kFlutterRoot, 'dev', 'tools', 'aatool', 'pubspec.yaml'))
+            .createSync();
+      });
+
+      testUsingContext('', () {
+        final List<String> packagePaths = runner.getRepoPackages()
+            .map((Directory d) => d.path).toList();
+        expect(packagePaths, <String>[
+          fs.directory(fs.path.join(_kFlutterRoot, 'dev', 'tools', 'aatool')).path,
+          fs.directory(fs.path.join(_kFlutterRoot, 'dev', 'tools')).path,
+        ]);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Platform: () => platform,
+      }, initializeFlutterRoot: false);
+    });
+
+    group('wrapping', () {
+      testUsingContext('checks that output wrapping is turned on when writing to a terminal', () async {
+        final FakeCommand fakeCommand = FakeCommand();
+        runner.addCommand(fakeCommand);
+        await runner.run(<String>['fake']);
+        expect(fakeCommand.preferences.wrapText, isTrue);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Stdio: () => FakeStdio(hasFakeTerminal: true),
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('checks that output wrapping is turned off when not writing to a terminal', () async {
+        final FakeCommand fakeCommand = FakeCommand();
+        runner.addCommand(fakeCommand);
+        await runner.run(<String>['fake']);
+        expect(fakeCommand.preferences.wrapText, isFalse);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Stdio: () => FakeStdio(hasFakeTerminal: false),
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('checks that output wrapping is turned off when set on the command line and writing to a terminal', () async {
+        final FakeCommand fakeCommand = FakeCommand();
+        runner.addCommand(fakeCommand);
+        await runner.run(<String>['--no-wrap', 'fake']);
+        expect(fakeCommand.preferences.wrapText, isFalse);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Stdio: () => FakeStdio(hasFakeTerminal: true),
+      }, initializeFlutterRoot: false);
+
+      testUsingContext('checks that output wrapping is turned on when set on the command line, but not writing to a terminal', () async {
+        final FakeCommand fakeCommand = FakeCommand();
+        runner.addCommand(fakeCommand);
+        await runner.run(<String>['--wrap', 'fake']);
+        expect(fakeCommand.preferences.wrapText, isTrue);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        Stdio: () => FakeStdio(hasFakeTerminal: false),
+      }, initializeFlutterRoot: false);
+    });
+  });
+}
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class FakeFlutterVersion extends FlutterVersion {
+  @override
+  String get frameworkVersion => '0.10.3';
+}
+
+class FakeCommand extends FlutterCommand {
+  OutputPreferences preferences;
+
+  @override
+  Future<FlutterCommandResult> runCommand() {
+    preferences = outputPreferences;
+    return Future<FlutterCommandResult>.value(const FlutterCommandResult(ExitStatus.success));
+  }
+
+  @override
+  String get description => null;
+
+  @override
+  String get name => 'fake';
+}
+
+class FakeStdio extends Stdio {
+  FakeStdio({this.hasFakeTerminal});
+
+  final bool hasFakeTerminal;
+
+  @override
+  bool get hasTerminal => hasFakeTerminal;
+
+  @override
+  int get terminalColumns => hasFakeTerminal ? 80 : null;
+
+  @override
+  int get terminalLines => hasFakeTerminal ? 24 : null;
+  @override
+  bool get supportsAnsiEscapes => hasFakeTerminal;
+}
diff --git a/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
new file mode 100644
index 0000000..86ee99d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
@@ -0,0 +1,311 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/base/time.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import 'utils.dart';
+
+void main() {
+  group('Flutter Command', () {
+    MockitoCache cache;
+    MockitoUsage usage;
+    MockClock clock;
+    List<int> mockTimes;
+
+    setUp(() {
+      cache = MockitoCache();
+      usage = MockitoUsage();
+      clock = MockClock();
+      when(usage.isFirstRun).thenReturn(false);
+      when(clock.now()).thenAnswer(
+        (Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
+      );
+    });
+
+    testUsingContext('honors shouldUpdateCache false', () async {
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: false);
+      await flutterCommand.run();
+      verifyZeroInteractions(cache);
+    },
+    overrides: <Type, Generator>{
+      Cache: () => cache,
+    });
+
+    testUsingContext('honors shouldUpdateCache true', () async {
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
+      await flutterCommand.run();
+      verify(cache.updateAll(any)).called(1);
+    },
+    overrides: <Type, Generator>{
+      Cache: () => cache,
+    });
+
+    testUsingContext('reports command that results in success', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.success);
+        }
+      );
+      await flutterCommand.run();
+
+      expect(
+        verify(usage.sendCommand(captureAny,
+                parameters: captureAnyNamed('parameters'))).captured,
+        <dynamic>[
+          'dummy',
+          const <String, String>{'cd26': 'success'}
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in warning', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.warning);
+        }
+      );
+      await flutterCommand.run();
+
+      expect(
+        verify(usage.sendCommand(captureAny,
+                parameters: captureAnyNamed('parameters'))).captured,
+        <dynamic>[
+          'dummy',
+          const <String, String>{'cd26': 'warning'}
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in failure', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          return const FlutterCommandResult(ExitStatus.fail);
+        }
+      );
+
+      try {
+        await flutterCommand.run();
+      } on ToolExit {
+        expect(
+          verify(usage.sendCommand(captureAny,
+                  parameters: captureAnyNamed('parameters'))).captured,
+          <dynamic>[
+            'dummy',
+            const <String, String>{'cd26': 'fail'}
+          ],
+        );
+      }
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('reports command that results in error', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          throwToolExit('fail');
+          return null; // unreachable
+        }
+      );
+
+      try {
+        await flutterCommand.run();
+        fail('Mock should make this fail');
+      } on ToolExit {
+        expect(
+          verify(usage.sendCommand(captureAny,
+                  parameters: captureAnyNamed('parameters'))).captured,
+          <dynamic>[
+            'dummy',
+            const <String, String>{'cd26': 'fail'}
+          ],
+        );
+      }
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('report execution timing by default', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
+      await flutterCommand.run();
+      verify(clock.now()).called(2);
+
+      expect(
+        verify(usage.sendTiming(
+                captureAny, captureAny, captureAny,
+                label: captureAnyNamed('label'))).captured,
+        <dynamic>[
+          'flutter',
+          'dummy',
+          const Duration(milliseconds: 1000),
+          null
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('no timing report without usagePath', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand =
+          DummyFlutterCommand(noUsagePath: true);
+      await flutterCommand.run();
+      verify(clock.now()).called(2);
+      verifyNever(usage.sendTiming(
+                   any, any, any,
+                   label: anyNamed('label')));
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('report additional FlutterCommandResult data', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final FlutterCommandResult commandResult = FlutterCommandResult(
+        ExitStatus.success,
+        // nulls should be cleaned up.
+        timingLabelParts: <String> ['blah1', 'blah2', null, 'blah3'],
+        endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
+      );
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async => commandResult
+      );
+      await flutterCommand.run();
+      verify(clock.now()).called(2);
+      expect(
+        verify(usage.sendTiming(
+                captureAny, captureAny, captureAny,
+                label: captureAnyNamed('label'))).captured,
+        <dynamic>[
+          'flutter',
+          'dummy',
+          const Duration(milliseconds: 500), // FlutterCommandResult's end time used instead.
+          'success-blah1-blah2-blah3',
+        ],
+      );
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+    testUsingContext('report failed execution timing too', () async {
+      // Crash if called a third time which is unexpected.
+      mockTimes = <int>[1000, 2000];
+
+      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
+        commandFunction: () async {
+          throwToolExit('fail');
+          return null; // unreachable
+        },
+      );
+
+      try {
+        await flutterCommand.run();
+        fail('Mock should make this fail');
+      } on ToolExit {
+        // Should have still checked time twice.
+        verify(clock.now()).called(2);
+
+        expect(
+          verify(usage.sendTiming(
+                  captureAny, captureAny, captureAny,
+                  label: captureAnyNamed('label'))).captured,
+          <dynamic>[
+            'flutter',
+            'dummy',
+            const Duration(milliseconds: 1000),
+            'fail',
+          ],
+        );
+      }
+    },
+    overrides: <Type, Generator>{
+      SystemClock: () => clock,
+      Usage: () => usage,
+    });
+
+  });
+
+  group('Experimental commands', () {
+    final MockVersion stableVersion = MockVersion();
+    final MockVersion betaVersion = MockVersion();
+    final FakeCommand fakeCommand = FakeCommand();
+    when(stableVersion.isMaster).thenReturn(false);
+    when(betaVersion.isMaster).thenReturn(true);
+
+    testUsingContext('Can be disabled on stable branch', () async {
+      expect(() => fakeCommand.run(), throwsA(isA<ToolExit>()));
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => stableVersion,
+    });
+
+    testUsingContext('Works normally on regular branches', () async {
+      expect(fakeCommand.run(), completes);
+    }, overrides: <Type, Generator>{
+      FlutterVersion: () => betaVersion,
+    });
+  });
+}
+
+
+class FakeCommand extends FlutterCommand {
+  @override
+  String get description => null;
+
+  @override
+  String get name => 'fake';
+
+  @override
+  bool get isExperimental => true;
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    return null;
+  }
+}
+
+class MockVersion extends Mock implements FlutterVersion {}
diff --git a/packages/flutter_tools/test/general.shard/runner/utils.dart b/packages/flutter_tools/test/general.shard/runner/utils.dart
new file mode 100644
index 0000000..e69e50d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/runner/utils.dart
@@ -0,0 +1,45 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/usage.dart';
+import 'package:flutter_tools/src/runner/flutter_command.dart';
+import 'package:mockito/mockito.dart';
+
+typedef CommandFunction = Future<FlutterCommandResult> Function();
+
+class DummyFlutterCommand extends FlutterCommand {
+
+  DummyFlutterCommand({
+    this.shouldUpdateCache = false,
+    this.noUsagePath  = false,
+    this.commandFunction,
+  });
+
+  final bool noUsagePath;
+  final CommandFunction commandFunction;
+
+  @override
+  final bool shouldUpdateCache;
+
+  @override
+  String get description => 'does nothing';
+
+  @override
+  Future<String> get usagePath => noUsagePath ? null : super.usagePath;
+
+  @override
+  String get name => 'dummy';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    return commandFunction == null ? null : await commandFunction();
+  }
+}
+
+class MockitoCache extends Mock implements Cache {}
+
+class MockitoUsage extends Mock implements Usage {}
diff --git a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart
new file mode 100644
index 0000000..7f4a2f4
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart
@@ -0,0 +1,406 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// 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: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';
+import '../src/context.dart';
+
+void main() {
+  TestRunner createTestRunner() {
+    // TODO(jacobr): make these tests run with `trackWidgetCreation: true` as
+    // well as the default flags.
+    return TestRunner(
+      <FlutterDevice>[FlutterDevice(MockDevice(), trackWidgetCreation: false, buildMode: BuildMode.debug)],
+    );
+  }
+
+  group('keyboard input handling', () {
+    testUsingContext('single help character', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.hasHelpBeenPrinted, false);
+      await terminalHandler.processTerminalInput('h');
+      expect(testRunner.hasHelpBeenPrinted, true);
+    });
+
+    testUsingContext('help character surrounded with newlines', () async {
+      final TestRunner testRunner = createTestRunner();
+      final TerminalHandler terminalHandler = TerminalHandler(testRunner);
+      expect(testRunner.hasHelpBeenPrinted, false);
+      await terminalHandler.processTerminalInput('\nh\n');
+      expect(testRunner.hasHelpBeenPrinted, true);
+    });
+  });
+
+  group('keycode verification, brought to you by the letter', () {
+    MockResidentRunner mockResidentRunner;
+    TerminalHandler terminalHandler;
+
+    setUp(() {
+      mockResidentRunner = MockResidentRunner();
+      terminalHandler = TerminalHandler(mockResidentRunner);
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
+    });
+
+    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 {
+      await terminalHandler.processTerminalInput('a');
+
+      verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
+    });
+
+    testUsingContext('a - debugToggleProfileWidgetBuilds without service protocol', () async {
+       when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('a');
+
+      verifyNever(mockResidentRunner.debugToggleProfileWidgetBuilds());
+    });
+
+
+    testUsingContext('a - debugToggleProfileWidgetBuilds', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(true);
+      await terminalHandler.processTerminalInput('a');
+
+      verify(mockResidentRunner.debugToggleProfileWidgetBuilds()).called(1);
+    });
+
+    testUsingContext('d,D - detach', () async {
+      await terminalHandler.processTerminalInput('d');
+      await terminalHandler.processTerminalInput('D');
+
+      verify(mockResidentRunner.detach()).called(2);
+    });
+
+    testUsingContext('h,H,? - printHelp', () async {
+      await terminalHandler.processTerminalInput('h');
+      await terminalHandler.processTerminalInput('H');
+      await terminalHandler.processTerminalInput('?');
+
+      verify(mockResidentRunner.printHelp(details: true)).called(3);
+    });
+
+    testUsingContext('i, I - debugToggleWidgetInspector with service protocol', () async {
+      await terminalHandler.processTerminalInput('i');
+      await terminalHandler.processTerminalInput('I');
+
+      verify(mockResidentRunner.debugToggleWidgetInspector()).called(2);
+    });
+
+    testUsingContext('i, I - debugToggleWidgetInspector without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('i');
+      await terminalHandler.processTerminalInput('I');
+
+      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');
+
+      verify(mockResidentRunner.debugDumpLayerTree()).called(1);
+    });
+
+    testUsingContext('L - debugDumpLayerTree without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('L');
+
+      verifyNever(mockResidentRunner.debugDumpLayerTree());
+    });
+
+    testUsingContext('o,O - debugTogglePlatform with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('o');
+      await terminalHandler.processTerminalInput('O');
+
+      verify(mockResidentRunner.debugTogglePlatform()).called(2);
+    });
+
+    testUsingContext('o,O - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('o');
+      await terminalHandler.processTerminalInput('O');
+
+      verifyNever(mockResidentRunner.debugTogglePlatform());
+    });
+
+    testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
+    });
+
+    testUsingContext('p - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
+    });
+
+    testUsingContext('p - debugToggleDebugPaintSizeEnabled with service protocol and debug mode', () async {
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verify(mockResidentRunner.debugToggleDebugPaintSizeEnabled()).called(1);
+    });
+
+    testUsingContext('p - debugTogglePlatform without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      await terminalHandler.processTerminalInput('p');
+
+      verifyNever(mockResidentRunner.debugToggleDebugPaintSizeEnabled());
+    });
+
+    testUsingContext('P - debugTogglePerformanceOverlayOverride with service protocol', () async {
+      await terminalHandler.processTerminalInput('P');
+
+      verify(mockResidentRunner.debugTogglePerformanceOverlayOverride()).called(1);
+    });
+
+    testUsingContext('P - debugTogglePerformanceOverlayOverride without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('P');
+
+      verifyNever(mockResidentRunner.debugTogglePerformanceOverlayOverride());
+    });
+
+    testUsingContext('q,Q - exit', () async {
+      await terminalHandler.processTerminalInput('q');
+      await terminalHandler.processTerminalInput('Q');
+
+      verify(mockResidentRunner.exit()).called(2);
+    });
+
+    testUsingContext('s - screenshot', () async {
+      final MockDevice mockDevice = MockDevice();
+      final MockFlutterDevice mockFlutterDevice = MockFlutterDevice();
+      when(mockResidentRunner.isRunningDebug).thenReturn(true);
+      when(mockResidentRunner.flutterDevices).thenReturn(<FlutterDevice>[mockFlutterDevice]);
+      when(mockFlutterDevice.device).thenReturn(mockDevice);
+      when(mockDevice.supportsScreenshot).thenReturn(true);
+
+      await terminalHandler.processTerminalInput('s');
+
+      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');
+
+      verify(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder()).called(1);
+    });
+
+    testUsingContext('S - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('S');
+
+      verifyNever(mockResidentRunner.debugDumpSemanticsTreeInTraversalOrder());
+    });
+
+    testUsingContext('t,T - debugDumpRenderTree with service protocol', () async {
+      await terminalHandler.processTerminalInput('t');
+      await terminalHandler.processTerminalInput('T');
+
+      verify(mockResidentRunner.debugDumpRenderTree()).called(2);
+    });
+
+    testUsingContext('t,T - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('t');
+      await terminalHandler.processTerminalInput('T');
+
+      verifyNever(mockResidentRunner.debugDumpRenderTree());
+    });
+
+    testUsingContext('U - debugDumpRenderTree with service protocol', () async {
+      await terminalHandler.processTerminalInput('U');
+
+      verify(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1);
+    });
+
+    testUsingContext('U - debugDumpSemanticsTreeInTraversalOrder without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('U');
+
+      verifyNever(mockResidentRunner.debugDumpSemanticsTreeInInverseHitTestOrder());
+    });
+
+    testUsingContext('w,W - debugDumpApp with service protocol', () async {
+      await terminalHandler.processTerminalInput('w');
+      await terminalHandler.processTerminalInput('W');
+
+      verify(mockResidentRunner.debugDumpApp()).called(2);
+    });
+
+    testUsingContext('w,W - debugDumpApp without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('w');
+      await terminalHandler.processTerminalInput('W');
+
+      verifyNever(mockResidentRunner.debugDumpApp());
+    });
+
+    testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled with service protocol', () async {
+      await terminalHandler.processTerminalInput('z');
+      await terminalHandler.processTerminalInput('Z');
+
+      verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
+    });
+
+    testUsingContext('z,Z - debugToggleDebugCheckElevationsEnabled without service protocol', () async {
+      when(mockResidentRunner.supportsServiceProtocol).thenReturn(false);
+      await terminalHandler.processTerminalInput('z');
+      await terminalHandler.processTerminalInput('Z');
+
+      // This should probably be disable when the service protocol is not enabled.
+      verify(mockResidentRunner.debugToggleDebugCheckElevationsEnabled()).called(2);
+    });
+  });
+}
+
+class MockDevice extends Mock implements Device {
+  MockDevice() {
+    when(isSupported()).thenReturn(true);
+  }
+}
+
+class MockResidentRunner extends Mock implements ResidentRunner {}
+
+class MockFlutterDevice extends Mock implements FlutterDevice {}
+
+class TestRunner extends ResidentRunner {
+  TestRunner(List<FlutterDevice> devices)
+    : super(devices);
+
+  bool hasHelpBeenPrinted = false;
+  String receivedCommand;
+
+  @override
+  Future<void> cleanupAfterSignal() async { }
+
+  @override
+  Future<void> cleanupAtFinish() async { }
+
+  @override
+  void printHelp({ bool details }) {
+    hasHelpBeenPrinted = true;
+  }
+
+  @override
+  Future<int> run({
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<void> appStartedCompleter,
+    String route,
+    bool shouldBuild = true,
+  }) async => null;
+
+  @override
+  Future<int> attach({
+    Completer<DebugConnectionInfo> connectionInfoCompleter,
+    Completer<void> appStartedCompleter,
+  }) async => null;
+}
diff --git a/packages/flutter_tools/test/general.shard/test_compiler_test.dart b/packages/flutter_tools/test/general.shard/test_compiler_test.dart
new file mode 100644
index 0000000..2158adf
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/test_compiler_test.dart
@@ -0,0 +1,91 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/compile.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/test/test_compiler.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/testbed.dart';
+
+void main() {
+  group(TestCompiler, () {
+    Testbed testbed;
+    FakeTestCompiler testCompiler;
+    MockResidentCompiler residentCompiler;
+
+    setUp(() {
+      testbed = Testbed(
+        setup: () async {
+          fs.file('pubspec.yaml').createSync();
+          fs.file('.packages').createSync();
+          fs.file('test/foo.dart').createSync(recursive: true);
+          residentCompiler = MockResidentCompiler();
+          testCompiler = FakeTestCompiler(
+            false,
+            FlutterProject.current(),
+            residentCompiler,
+          );
+        },
+      );
+    });
+
+    test('Reports a dill file when compile is successful', () => testbed.run(() async {
+      when(residentCompiler.recompile(
+        'test/foo.dart',
+        <Uri>[Uri.parse('test/foo.dart')],
+        outputPath: testCompiler.outputDill.path,
+      )).thenAnswer((Invocation invocation) async {
+        fs.file('abc.dill').createSync();
+        return const CompilerOutput('abc.dill', 0, <Uri>[]);
+      });
+
+      expect(await testCompiler.compile('test/foo.dart'), 'test/foo.dart.dill');
+      expect(fs.file('test/foo.dart.dill').existsSync(), true);
+    }));
+
+    test('Reports null when a compile fails', () => testbed.run(() async {
+      when(residentCompiler.recompile(
+        'test/foo.dart',
+        <Uri>[Uri.parse('test/foo.dart')],
+        outputPath: testCompiler.outputDill.path,
+      )).thenAnswer((Invocation invocation) async {
+        fs.file('abc.dill').createSync();
+        return const CompilerOutput('abc.dill', 1, <Uri>[]);
+      });
+
+      expect(await testCompiler.compile('test/foo.dart'), null);
+      expect(fs.file('test/foo.dart.dill').existsSync(), false);
+      verify(residentCompiler.shutdown()).called(1);
+    }));
+
+    test('Disposing test compiler shuts down backing compiler', () => testbed.run(() async {
+      testCompiler.compiler = residentCompiler;
+      expect(testCompiler.compilerController.isClosed, false);
+      await testCompiler.dispose();
+      expect(testCompiler.compilerController.isClosed, true);
+      verify(residentCompiler.shutdown()).called(1);
+    }));
+  });
+}
+
+/// Override the creation of the Resident Compiler to simplify testing.
+class FakeTestCompiler extends TestCompiler {
+  FakeTestCompiler(
+    bool trackWidgetCreation,
+    FlutterProject flutterProject,
+    this.residentCompiler,
+  ) : super(trackWidgetCreation, flutterProject);
+
+  final MockResidentCompiler residentCompiler;
+
+  @override
+  Future<ResidentCompiler> createCompiler() async {
+    return residentCompiler;
+  }
+}
+
+class MockResidentCompiler extends Mock implements ResidentCompiler {}
diff --git a/packages/flutter_tools/test/general.shard/testbed_test.dart b/packages/flutter_tools/test/general.shard/testbed_test.dart
new file mode 100644
index 0000000..814bdee
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/testbed_test.dart
@@ -0,0 +1,90 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import '../src/common.dart';
+import '../src/testbed.dart';
+
+void main() {
+  group('Testbed', () {
+
+    test('Can provide default interfaces', () async {
+      final Testbed testbed = Testbed();
+
+      FileSystem localFileSystem;
+      await testbed.run(() {
+        localFileSystem = fs;
+      });
+
+      expect(localFileSystem, isA<MemoryFileSystem>());
+    });
+
+    test('Can provide setup interfaces', () async {
+      final Testbed testbed = Testbed(overrides: <Type, Generator>{
+        A: () => A(),
+      });
+
+      A instance;
+      await testbed.run(() {
+        instance = context.get<A>();
+      });
+
+      expect(instance, isA<A>());
+    });
+
+    test('Can provide local overrides', () async {
+      final Testbed testbed = Testbed(overrides: <Type, Generator>{
+        A: () => A(),
+      });
+
+      A instance;
+      await testbed.run(() {
+        instance = context.get<A>();
+      }, overrides: <Type, Generator>{
+        A: () => B(),
+      });
+
+      expect(instance, isA<B>());
+    });
+
+    test('provides a mocked http client', () async {
+      final Testbed testbed = Testbed();
+      await testbed.run(() async {
+        final HttpClient client = HttpClient();
+        final HttpClientRequest request = await client.getUrl(null);
+        final HttpClientResponse response = await request.close();
+
+        expect(response.statusCode, HttpStatus.badRequest);
+        expect(response.contentLength, 0);
+      });
+    });
+
+    test('Throws StateError if Timer is left pending', () async {
+      final Testbed testbed = Testbed();
+
+      expect(testbed.run(() async {
+        Timer.periodic(const Duration(seconds: 1), (Timer timer) { });
+      }), throwsA(isInstanceOf<StateError>()));
+    });
+
+    test('Doesnt throw a StateError if Timer is left cleaned up', () async {
+      final Testbed testbed = Testbed();
+
+      testbed.run(() async {
+        final Timer timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) { });
+        timer.cancel();
+      });
+    });
+  });
+}
+
+class A {}
+
+class B extends A {}
diff --git a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart
new file mode 100644
index 0000000..614df14
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart
@@ -0,0 +1,206 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// 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/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/compile.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/tester/flutter_tester.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  MemoryFileSystem fs;
+
+  setUp(() {
+    fs = MemoryFileSystem();
+  });
+
+  group('FlutterTesterApp', () {
+    testUsingContext('fromCurrentDirectory', () async {
+      const String projectPath = '/home/my/projects/my_project';
+      await fs.directory(projectPath).create(recursive: true);
+      fs.currentDirectory = projectPath;
+
+      final FlutterTesterApp app = FlutterTesterApp.fromCurrentDirectory();
+      expect(app.name, 'my_project');
+      expect(app.packagesFile.path, fs.path.join(projectPath, '.packages'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => fs,
+    });
+  });
+
+  group('FlutterTesterDevices', () {
+    tearDown(() {
+      FlutterTesterDevices.showFlutterTesterDevice = false;
+    });
+
+    testUsingContext('no device', () async {
+      final FlutterTesterDevices discoverer = FlutterTesterDevices();
+
+      final List<Device> devices = await discoverer.devices;
+      expect(devices, isEmpty);
+    });
+
+    testUsingContext('has device', () async {
+      FlutterTesterDevices.showFlutterTesterDevice = true;
+      final FlutterTesterDevices discoverer = FlutterTesterDevices();
+
+      final List<Device> devices = await discoverer.devices;
+      expect(devices, hasLength(1));
+
+      final Device device = devices.single;
+      expect(device, isInstanceOf<FlutterTesterDevice>());
+      expect(device.id, 'flutter-tester');
+    });
+  });
+
+  group('FlutterTesterDevice', () {
+    FlutterTesterDevice device;
+    List<String> logLines;
+
+    setUp(() {
+      device = FlutterTesterDevice('flutter-tester');
+
+      logLines = <String>[];
+      device.getLogReader().logLines.listen(logLines.add);
+    });
+
+    testUsingContext('getters', () async {
+      expect(device.id, 'flutter-tester');
+      expect(await device.isLocalEmulator, isFalse);
+      expect(device.name, 'Flutter test device');
+      expect(device.portForwarder, isNot(isNull));
+      expect(await device.targetPlatform, TargetPlatform.tester);
+
+      expect(await device.installApp(null), isTrue);
+      expect(await device.isAppInstalled(null), isFalse);
+      expect(await device.isLatestBuildInstalled(null), isFalse);
+      expect(await device.uninstallApp(null), isTrue);
+
+      expect(device.isSupported(), isTrue);
+    });
+
+    group('startApp', () {
+      String flutterRoot;
+      String flutterTesterPath;
+
+      String projectPath;
+      String mainPath;
+
+      MockArtifacts mockArtifacts;
+      MockKernelCompiler mockKernelCompiler;
+      MockProcessManager mockProcessManager;
+      MockProcess mockProcess;
+
+      final Map<Type, Generator> startOverrides = <Type, Generator>{
+        Platform: () => FakePlatform(operatingSystem: 'linux'),
+        FileSystem: () => fs,
+        Cache: () => Cache(rootOverride: fs.directory(flutterRoot)),
+        ProcessManager: () => mockProcessManager,
+        KernelCompilerFactory: () => FakeKernelCompilerFactory(mockKernelCompiler),
+        Artifacts: () => mockArtifacts,
+      };
+
+      setUp(() {
+        flutterRoot = fs.path.join('home', 'me', 'flutter');
+        flutterTesterPath = fs.path.join(flutterRoot, 'bin', 'cache',
+            'artifacts', 'engine', 'linux-x64', 'flutter_tester');
+
+        final File flutterTesterFile = fs.file(flutterTesterPath);
+        flutterTesterFile.parent.createSync(recursive: true);
+        flutterTesterFile.writeAsBytesSync(const <int>[]);
+
+        projectPath = fs.path.join('home', 'me', 'hello');
+        mainPath = fs.path.join(projectPath, 'lin', 'main.dart');
+
+        mockProcessManager = MockProcessManager();
+        mockProcessManager.processFactory =
+            (List<String> commands) => mockProcess;
+
+        mockArtifacts = MockArtifacts();
+        final String artifactPath = fs.path.join(flutterRoot, 'artifact');
+        fs.file(artifactPath).createSync(recursive: true);
+        when(mockArtifacts.getArtifactPath(any)).thenReturn(artifactPath);
+
+        mockKernelCompiler = MockKernelCompiler();
+      });
+
+      testUsingContext('not debug', () async {
+        final LaunchResult result = await device.startApp(null,
+            mainPath: mainPath,
+            debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null)));
+        expect(result.started, isFalse);
+      }, overrides: startOverrides);
+
+      testUsingContext('no flutter_tester', () async {
+        fs.file(flutterTesterPath).deleteSync();
+        expect(() async {
+          await device.startApp(null,
+              mainPath: mainPath,
+              debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null)));
+        }, throwsToolExit());
+      }, overrides: startOverrides);
+
+      testUsingContext('start', () async {
+        final Uri observatoryUri = Uri.parse('http://127.0.0.1:6666/');
+        mockProcess = MockProcess(
+            stdout: Stream<List<int>>.fromIterable(<List<int>>[
+          '''
+Observatory listening on $observatoryUri
+Hello!
+'''
+              .codeUnits,
+        ]));
+
+        when(mockKernelCompiler.compile(
+          sdkRoot: anyNamed('sdkRoot'),
+          incrementalCompilerByteStorePath: anyNamed('incrementalCompilerByteStorePath'),
+          mainPath: anyNamed('mainPath'),
+          outputFilePath: anyNamed('outputFilePath'),
+          depFilePath: anyNamed('depFilePath'),
+          trackWidgetCreation: anyNamed('trackWidgetCreation'),
+          extraFrontEndOptions: anyNamed('extraFrontEndOptions'),
+          fileSystemRoots: anyNamed('fileSystemRoots'),
+          fileSystemScheme: anyNamed('fileSystemScheme'),
+          packagesPath: anyNamed('packagesPath'),
+        )).thenAnswer((_) async {
+          fs.file('$mainPath.dill').createSync(recursive: true);
+          return CompilerOutput('$mainPath.dill', 0, <Uri>[]);
+        });
+
+        final LaunchResult result = await device.startApp(null,
+            mainPath: mainPath,
+            debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null)));
+        expect(result.started, isTrue);
+        expect(result.observatoryUri, observatoryUri);
+        expect(logLines.last, 'Hello!');
+      }, overrides: startOverrides);
+    });
+  });
+}
+
+class MockArtifacts extends Mock implements Artifacts {}
+class MockKernelCompiler extends Mock implements KernelCompiler {}
+class FakeKernelCompilerFactory implements KernelCompilerFactory {
+  FakeKernelCompilerFactory(this.kernelCompiler);
+
+  final KernelCompiler kernelCompiler;
+
+  @override
+  Future<KernelCompiler> create(FlutterProject flutterProject) async {
+    return kernelCompiler;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/time_test.dart b/packages/flutter_tools/test/general.shard/time_test.dart
new file mode 100644
index 0000000..e5e53e9
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/time_test.dart
@@ -0,0 +1,21 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/time.dart';
+
+import '../src/common.dart';
+
+void main() {
+  group(SystemClock, () {
+    test('can set a fixed time', () {
+      final SystemClock clock = SystemClock.fixed(DateTime(1991, 8, 23));
+      expect(clock.now(), DateTime(1991, 8, 23));
+    });
+
+    test('can find a time ago', () {
+      final SystemClock clock = SystemClock.fixed(DateTime(1991, 8, 23));
+      expect(clock.ago(const Duration(days: 10)), DateTime(1991, 8, 13));
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/utils_test.dart b/packages/flutter_tools/test/general.shard/utils_test.dart
new file mode 100644
index 0000000..9a2cb33
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/utils_test.dart
@@ -0,0 +1,372 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// 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:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/utils.dart';
+import 'package:flutter_tools/src/base/version.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+void main() {
+  group('SettingsFile', () {
+    test('parse', () {
+      final SettingsFile file = SettingsFile.parse('''
+# ignore comment
+foo=bar
+baz=qux
+''');
+      expect(file.values['foo'], 'bar');
+      expect(file.values['baz'], 'qux');
+      expect(file.values, hasLength(2));
+    });
+  });
+
+  group('uuid', () {
+    // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
+    test('simple', () {
+      final Uuid uuid = Uuid();
+      final String result = uuid.generateV4();
+      expect(result.length, 36);
+      expect(result[8], '-');
+      expect(result[13], '-');
+      expect(result[18], '-');
+      expect(result[23], '-');
+    });
+
+    test('can parse', () {
+      final Uuid uuid = Uuid();
+      final String result = uuid.generateV4();
+      expect(int.parse(result.substring(0, 8), radix: 16), isNotNull);
+      expect(int.parse(result.substring(9, 13), radix: 16), isNotNull);
+      expect(int.parse(result.substring(14, 18), radix: 16), isNotNull);
+      expect(int.parse(result.substring(19, 23), radix: 16), isNotNull);
+      expect(int.parse(result.substring(24, 36), radix: 16), isNotNull);
+    });
+
+    test('special bits', () {
+      final Uuid uuid = Uuid();
+      String result = uuid.generateV4();
+      expect(result[14], '4');
+      expect(result[19].toLowerCase(), isIn('89ab'));
+
+      result = uuid.generateV4();
+      expect(result[19].toLowerCase(), isIn('89ab'));
+
+      result = uuid.generateV4();
+      expect(result[19].toLowerCase(), isIn('89ab'));
+    });
+
+    test('is pretty random', () {
+      final Set<String> set = <String>{};
+
+      Uuid uuid = Uuid();
+      for (int i = 0; i < 64; i++) {
+        final String val = uuid.generateV4();
+        expect(set, isNot(contains(val)));
+        set.add(val);
+      }
+
+      uuid = Uuid();
+      for (int i = 0; i < 64; i++) {
+        final String val = uuid.generateV4();
+        expect(set, isNot(contains(val)));
+        set.add(val);
+      }
+
+      uuid = Uuid();
+      for (int i = 0; i < 64; i++) {
+        final String val = uuid.generateV4();
+        expect(set, isNot(contains(val)));
+        set.add(val);
+      }
+    });
+  });
+
+  group('Version', () {
+    test('can parse and compare', () {
+      expect(Version.unknown.toString(), equals('unknown'));
+      expect(Version(null, null, null).toString(), equals('0'));
+
+      final Version v1 = Version.parse('1');
+      expect(v1.major, equals(1));
+      expect(v1.minor, equals(0));
+      expect(v1.patch, equals(0));
+
+      expect(v1, greaterThan(Version.unknown));
+
+      final Version v2 = Version.parse('1.2');
+      expect(v2.major, equals(1));
+      expect(v2.minor, equals(2));
+      expect(v2.patch, equals(0));
+
+      final Version v3 = Version.parse('1.2.3');
+      expect(v3.major, equals(1));
+      expect(v3.minor, equals(2));
+      expect(v3.patch, equals(3));
+
+      final Version v4 = Version.parse('1.12');
+      expect(v4, greaterThan(v2));
+
+      expect(v3, greaterThan(v2));
+      expect(v2, greaterThan(v1));
+
+      final Version v5 = Version(1, 2, 0, text: 'foo');
+      expect(v5, equals(v2));
+
+      expect(Version.parse('Preview2.2'), isNull);
+    });
+  });
+
+  group('Poller', () {
+    const Duration kShortDelay = Duration(milliseconds: 100);
+
+    Poller poller;
+
+    tearDown(() {
+      poller?.cancel();
+    });
+
+    test('fires at start', () async {
+      bool called = false;
+      poller = Poller(() async {
+        called = true;
+      }, const Duration(seconds: 1));
+      expect(called, false);
+      await Future<void>.delayed(kShortDelay);
+      expect(called, true);
+    });
+
+    test('runs periodically', () async {
+      // Ensure we get the first (no-delay) callback, and one of the periodic callbacks.
+      int callCount = 0;
+      poller = Poller(() async {
+        callCount++;
+      }, Duration(milliseconds: kShortDelay.inMilliseconds ~/ 2));
+      expect(callCount, 0);
+      await Future<void>.delayed(kShortDelay);
+      expect(callCount, greaterThanOrEqualTo(2));
+    });
+
+    test('no quicker then the periodic delay', () async {
+      // Make sure that the poller polls at delay + the time it took to run the callback.
+      final Completer<Duration> completer = Completer<Duration>();
+      DateTime firstTime;
+      poller = Poller(() async {
+        if (firstTime == null)
+          firstTime = DateTime.now();
+        else
+          completer.complete(DateTime.now().difference(firstTime));
+
+        // introduce a delay
+        await Future<void>.delayed(kShortDelay);
+      }, kShortDelay);
+      final Duration duration = await completer.future;
+      expect(
+          duration, greaterThanOrEqualTo(Duration(milliseconds: kShortDelay.inMilliseconds * 2)));
+    });
+  });
+
+  group('Misc', () {
+    test('snakeCase', () async {
+      expect(snakeCase('abc'), equals('abc'));
+      expect(snakeCase('abC'), equals('ab_c'));
+      expect(snakeCase('aBc'), equals('a_bc'));
+      expect(snakeCase('aBC'), equals('a_b_c'));
+      expect(snakeCase('Abc'), equals('abc'));
+      expect(snakeCase('AbC'), equals('ab_c'));
+      expect(snakeCase('ABc'), equals('a_bc'));
+      expect(snakeCase('ABC'), equals('a_b_c'));
+    });
+  });
+
+  group('text wrapping', () {
+    const int _lineLength = 40;
+    const String _longLine = 'This is a long line that needs to be wrapped.';
+    final String _longLineWithNewlines = 'This is a long line with newlines that\n'
+        'needs to be wrapped.\n\n' +
+        '0123456789' * 5;
+    final String _longAnsiLineWithNewlines = '${AnsiTerminal.red}This${AnsiTerminal.resetAll} is a long line with newlines that\n'
+        'needs to be wrapped.\n\n'
+        '${AnsiTerminal.green}0123456789${AnsiTerminal.resetAll}' +
+        '0123456789' * 3 +
+        '${AnsiTerminal.green}0123456789${AnsiTerminal.resetAll}';
+    const String _onlyAnsiSequences = '${AnsiTerminal.red}${AnsiTerminal.resetAll}';
+    final String _indentedLongLineWithNewlines = '    This is an indented long line with newlines that\n'
+        'needs to be wrapped.\n\tAnd preserves tabs.\n      \n  ' +
+        '0123456789' * 5;
+    const String _shortLine = 'Short line.';
+    const String _indentedLongLine = '    This is an indented long line that needs to be '
+        'wrapped and indentation preserved.';
+    final FakeStdio fakeStdio = FakeStdio();
+
+    void testWrap(String description, Function body) {
+      testUsingContext(description, body, overrides: <Type, Generator>{
+        OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: _lineLength),
+      });
+    }
+
+    void testNoWrap(String description, Function body) {
+      testUsingContext(description, body, overrides: <Type, Generator>{
+        OutputPreferences: () => OutputPreferences(wrapText: false),
+      });
+    }
+
+    test('does not wrap by default in tests', () {
+      expect(wrapText(_longLine), equals(_longLine));
+    });
+    testNoWrap('can override wrap preference if preference is off', () {
+      expect(wrapText(_longLine, columnWidth: _lineLength, shouldWrap: true), equals('''
+This is a long line that needs to be
+wrapped.'''));
+    });
+    testWrap('can override wrap preference if preference is on', () {
+      expect(wrapText(_longLine, shouldWrap: false), equals(_longLine));
+    });
+    testNoWrap('does not wrap at all if not told to wrap', () {
+      expect(wrapText(_longLine), equals(_longLine));
+    });
+    testWrap('does not wrap short lines.', () {
+      expect(wrapText(_shortLine, columnWidth: _lineLength), equals(_shortLine));
+    });
+    testWrap('able to wrap long lines', () {
+      expect(wrapText(_longLine, columnWidth: _lineLength), equals('''
+This is a long line that needs to be
+wrapped.'''));
+    });
+    testUsingContext('able to handle dynamically changing terminal column size', () {
+      fakeStdio.currentColumnSize = 20;
+      expect(wrapText(_longLine), equals('''
+This is a long line
+that needs to be
+wrapped.'''));
+      fakeStdio.currentColumnSize = _lineLength;
+      expect(wrapText(_longLine), equals('''
+This is a long line that needs to be
+wrapped.'''));
+    }, overrides: <Type, Generator>{
+      OutputPreferences: () => OutputPreferences(wrapText: true),
+      Stdio: () => fakeStdio,
+    });
+    testWrap('wrap long lines with no whitespace', () {
+      expect(wrapText('0123456789' * 5, columnWidth: _lineLength), equals('''
+0123456789012345678901234567890123456789
+0123456789'''));
+    });
+    testWrap('refuses to wrap to a column smaller than 10 characters', () {
+      expect(wrapText('$_longLine ' + '0123456789' * 4, columnWidth: 1), equals('''
+This is a
+long line
+that needs
+to be
+wrapped.
+0123456789
+0123456789
+0123456789
+0123456789'''));
+    });
+    testWrap('preserves indentation', () {
+      expect(wrapText(_indentedLongLine, columnWidth: _lineLength), equals('''
+    This is an indented long line that
+    needs to be wrapped and indentation
+    preserved.'''));
+    });
+    testWrap('preserves indentation and stripping trailing whitespace', () {
+      expect(wrapText('$_indentedLongLine   ', columnWidth: _lineLength), equals('''
+    This is an indented long line that
+    needs to be wrapped and indentation
+    preserved.'''));
+    });
+    testWrap('wraps text with newlines', () {
+      expect(wrapText(_longLineWithNewlines, columnWidth: _lineLength), equals('''
+This is a long line with newlines that
+needs to be wrapped.
+
+0123456789012345678901234567890123456789
+0123456789'''));
+    });
+    testWrap('wraps text with ANSI sequences embedded', () {
+      expect(wrapText(_longAnsiLineWithNewlines, columnWidth: _lineLength), equals('''
+${AnsiTerminal.red}This${AnsiTerminal.resetAll} is a long line with newlines that
+needs to be wrapped.
+
+${AnsiTerminal.green}0123456789${AnsiTerminal.resetAll}012345678901234567890123456789
+${AnsiTerminal.green}0123456789${AnsiTerminal.resetAll}'''));
+    });
+    testWrap('wraps text with only ANSI sequences', () {
+      expect(wrapText(_onlyAnsiSequences, columnWidth: _lineLength),
+          equals('${AnsiTerminal.red}${AnsiTerminal.resetAll}'));
+    });
+    testWrap('preserves indentation in the presence of newlines', () {
+      expect(wrapText(_indentedLongLineWithNewlines, columnWidth: _lineLength), equals('''
+    This is an indented long line with
+    newlines that
+needs to be wrapped.
+\tAnd preserves tabs.
+
+  01234567890123456789012345678901234567
+  890123456789'''));
+    });
+    testWrap('removes trailing whitespace when wrapping', () {
+      expect(wrapText('$_longLine     \t', columnWidth: _lineLength), equals('''
+This is a long line that needs to be
+wrapped.'''));
+    });
+    testWrap('honors hangingIndent parameter', () {
+      expect(wrapText(_longLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
+This is a long line that needs to be
+      wrapped.'''));
+    });
+    testWrap('handles hangingIndent with a single unwrapped line.', () {
+      expect(wrapText(_shortLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
+Short line.'''));
+    });
+    testWrap('handles hangingIndent with two unwrapped lines and the second is empty.', () {
+      expect(wrapText('$_shortLine\n', columnWidth: _lineLength, hangingIndent: 6), equals('''
+Short line.
+'''));
+    });
+    testWrap('honors hangingIndent parameter on already indented line.', () {
+      expect(wrapText(_indentedLongLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
+    This is an indented long line that
+          needs to be wrapped and
+          indentation preserved.'''));
+    });
+    testWrap('honors hangingIndent and indent parameters at the same time.', () {
+      expect(wrapText(_indentedLongLine, columnWidth: _lineLength, indent: 6, hangingIndent: 6), equals('''
+          This is an indented long line
+                that needs to be wrapped
+                and indentation
+                preserved.'''));
+    });
+    testWrap('honors indent parameter on already indented line.', () {
+      expect(wrapText(_indentedLongLine, columnWidth: _lineLength, indent: 6), equals('''
+          This is an indented long line
+          that needs to be wrapped and
+          indentation preserved.'''));
+    });
+    testWrap('honors hangingIndent parameter on already indented line.', () {
+      expect(wrapText(_indentedLongLineWithNewlines, columnWidth: _lineLength, hangingIndent: 6), equals('''
+    This is an indented long line with
+          newlines that
+needs to be wrapped.
+	And preserves tabs.
+
+  01234567890123456789012345678901234567
+        890123456789'''));
+    });
+  });
+}
+
+class FakeStdio extends Stdio {
+  FakeStdio();
+
+  int currentColumnSize = 20;
+
+  @override
+  int get terminalColumns => currentColumnSize;
+}
diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart
new file mode 100644
index 0000000..1a94470
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/version_test.dart
@@ -0,0 +1,527 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:collection/collection.dart' show ListEquality;
+import 'package:flutter_tools/src/base/time.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/version.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+
+final SystemClock _testClock = SystemClock.fixed(DateTime(2015, 1, 1));
+final DateTime _stampUpToDate = _testClock.ago(FlutterVersion.checkAgeConsideredUpToDate ~/ 2);
+final DateTime _stampOutOfDate = _testClock.ago(FlutterVersion.checkAgeConsideredUpToDate * 2);
+
+void main() {
+  MockProcessManager mockProcessManager;
+  MockCache mockCache;
+
+  setUp(() {
+    mockProcessManager = MockProcessManager();
+    mockCache = MockCache();
+  });
+
+  for (String channel in FlutterVersion.officialChannels) {
+    DateTime getChannelUpToDateVersion() {
+      return _testClock.ago(FlutterVersion.versionAgeConsideredUpToDate(channel) ~/ 2);
+    }
+
+    DateTime getChannelOutOfDateVersion() {
+      return _testClock.ago(FlutterVersion.versionAgeConsideredUpToDate(channel) * 2);
+    }
+
+    group('$FlutterVersion for $channel', () {
+      setUpAll(() {
+        Cache.disableLocking();
+        FlutterVersion.timeToPauseToLetUserReadTheMessage = Duration.zero;
+      });
+
+      testUsingContext('prints nothing when Flutter installation looks fresh', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelUpToDateVersion(),
+          // Server will be pinged because we haven't pinged within last x days
+          expectServerPing: true,
+          remoteCommitDate: getChannelOutOfDateVersion(),
+          expectSetStamp: true,
+          channel: channel,
+        );
+        await FlutterVersion.instance.checkFlutterVersionFreshness();
+        _expectVersionMessage('');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('prints nothing when Flutter installation looks out-of-date but is actually up-to-date', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          stamp: VersionCheckStamp(
+            lastTimeVersionWasChecked: _stampOutOfDate,
+            lastKnownRemoteVersion: getChannelOutOfDateVersion(),
+          ),
+          remoteCommitDate: getChannelOutOfDateVersion(),
+          expectSetStamp: true,
+          expectServerPing: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage('');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('does not ping server when version stamp is up-to-date', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          stamp: VersionCheckStamp(
+            lastTimeVersionWasChecked: _stampUpToDate,
+            lastKnownRemoteVersion: getChannelUpToDateVersion(),
+          ),
+          expectSetStamp: true,
+          channel: channel,
+        );
+
+        final FlutterVersion version = FlutterVersion.instance;
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('does not print warning if printed recently', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          stamp: VersionCheckStamp(
+            lastTimeVersionWasChecked: _stampUpToDate,
+            lastKnownRemoteVersion: getChannelUpToDateVersion(),
+          ),
+          expectSetStamp: true,
+          channel: channel,
+        );
+
+        final FlutterVersion version = FlutterVersion.instance;
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
+        expect((await VersionCheckStamp.load()).lastTimeWarningWasPrinted, _testClock.now());
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage('');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('pings server when version stamp is missing then does not', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          remoteCommitDate: getChannelUpToDateVersion(),
+          expectSetStamp: true,
+          expectServerPing: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
+
+        // Immediate subsequent check is not expected to ping the server.
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          stamp: await VersionCheckStamp.load(),
+          channel: channel,
+        );
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage('');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('pings server when version stamp is out-of-date', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          stamp: VersionCheckStamp(
+            lastTimeVersionWasChecked: _stampOutOfDate,
+            lastKnownRemoteVersion: _testClock.ago(const Duration(days: 2)),
+          ),
+          remoteCommitDate: getChannelUpToDateVersion(),
+          expectSetStamp: true,
+          expectServerPing: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage(FlutterVersion.newVersionAvailableMessage());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('does not print warning when unable to connect to server if not out of date', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelUpToDateVersion(),
+          errorOnFetch: true,
+          expectServerPing: true,
+          expectSetStamp: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage('');
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('prints warning when unable to connect to server if really out of date', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          errorOnFetch: true,
+          expectServerPing: true,
+          expectSetStamp: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        await version.checkFlutterVersionFreshness();
+        _expectVersionMessage(FlutterVersion.versionOutOfDateMessage(_testClock.now().difference(getChannelOutOfDateVersion())));
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('versions comparison', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          localCommitDate: getChannelOutOfDateVersion(),
+          errorOnFetch: true,
+          expectServerPing: true,
+          expectSetStamp: true,
+          channel: channel,
+        );
+        final FlutterVersion version = FlutterVersion.instance;
+
+        when(mockProcessManager.runSync(
+          <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'],
+          workingDirectory: anyNamed('workingDirectory'),
+        )).thenReturn(ProcessResult(1, 0, '', ''));
+
+        expect(
+            version.checkRevisionAncestry(
+              tentativeDescendantRevision: '123456',
+              tentativeAncestorRevision: 'abcdef',
+            ),
+            true);
+
+        verify(mockProcessManager.runSync(
+          <String>['git', 'merge-base', '--is-ancestor', 'abcdef', '123456'],
+          workingDirectory: anyNamed('workingDirectory'),
+        ));
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+      });
+    });
+
+    group('$VersionCheckStamp for $channel', () {
+      void _expectDefault(VersionCheckStamp stamp) {
+        expect(stamp.lastKnownRemoteVersion, isNull);
+        expect(stamp.lastTimeVersionWasChecked, isNull);
+        expect(stamp.lastTimeWarningWasPrinted, isNull);
+      }
+
+      testUsingContext('loads blank when stamp file missing', () async {
+        fakeData(mockProcessManager, mockCache, channel: channel);
+        _expectDefault(await VersionCheckStamp.load());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('loads blank when stamp file is malformed JSON', () async {
+        fakeData(mockProcessManager, mockCache, stampJson: '<', channel: channel);
+        _expectDefault(await VersionCheckStamp.load());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('loads blank when stamp file is well-formed but invalid JSON', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          stampJson: '[]',
+          channel: channel,
+        );
+        _expectDefault(await VersionCheckStamp.load());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('loads valid JSON', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          stampJson: '''
+      {
+        "lastKnownRemoteVersion": "${_testClock.ago(const Duration(days: 1))}",
+        "lastTimeVersionWasChecked": "${_testClock.ago(const Duration(days: 2))}",
+        "lastTimeWarningWasPrinted": "${_testClock.now()}"
+      }
+      ''',
+          channel: channel,
+        );
+
+        final VersionCheckStamp stamp = await VersionCheckStamp.load();
+        expect(stamp.lastKnownRemoteVersion, _testClock.ago(const Duration(days: 1)));
+        expect(stamp.lastTimeVersionWasChecked, _testClock.ago(const Duration(days: 2)));
+        expect(stamp.lastTimeWarningWasPrinted, _testClock.now());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('stores version stamp', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          expectSetStamp: true,
+          channel: channel,
+        );
+
+        _expectDefault(await VersionCheckStamp.load());
+
+        final VersionCheckStamp stamp = VersionCheckStamp(
+          lastKnownRemoteVersion: _testClock.ago(const Duration(days: 1)),
+          lastTimeVersionWasChecked: _testClock.ago(const Duration(days: 2)),
+          lastTimeWarningWasPrinted: _testClock.now(),
+        );
+        await stamp.store();
+
+        final VersionCheckStamp storedStamp = await VersionCheckStamp.load();
+        expect(storedStamp.lastKnownRemoteVersion, _testClock.ago(const Duration(days: 1)));
+        expect(storedStamp.lastTimeVersionWasChecked, _testClock.ago(const Duration(days: 2)));
+        expect(storedStamp.lastTimeWarningWasPrinted, _testClock.now());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+
+      testUsingContext('overwrites individual fields', () async {
+        fakeData(
+          mockProcessManager,
+          mockCache,
+          expectSetStamp: true,
+          channel: channel,
+        );
+
+        _expectDefault(await VersionCheckStamp.load());
+
+        final VersionCheckStamp stamp = VersionCheckStamp(
+          lastKnownRemoteVersion: _testClock.ago(const Duration(days: 10)),
+          lastTimeVersionWasChecked: _testClock.ago(const Duration(days: 9)),
+          lastTimeWarningWasPrinted: _testClock.ago(const Duration(days: 8)),
+        );
+        await stamp.store(
+          newKnownRemoteVersion: _testClock.ago(const Duration(days: 1)),
+          newTimeVersionWasChecked: _testClock.ago(const Duration(days: 2)),
+          newTimeWarningWasPrinted: _testClock.now(),
+        );
+
+        final VersionCheckStamp storedStamp = await VersionCheckStamp.load();
+        expect(storedStamp.lastKnownRemoteVersion, _testClock.ago(const Duration(days: 1)));
+        expect(storedStamp.lastTimeVersionWasChecked, _testClock.ago(const Duration(days: 2)));
+        expect(storedStamp.lastTimeWarningWasPrinted, _testClock.now());
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => FlutterVersion(_testClock),
+        ProcessManager: () => mockProcessManager,
+        Cache: () => mockCache,
+      });
+    });
+  }
+
+  testUsingContext('GitTagVersion', () {
+    const String hash = 'abcdef';
+    expect(GitTagVersion.parse('v1.2.3-4-g$hash').frameworkVersionFor(hash), '1.2.4-pre.4');
+    expect(GitTagVersion.parse('v98.76.54-32-g$hash').frameworkVersionFor(hash), '98.76.55-pre.32');
+    expect(GitTagVersion.parse('v10.20.30-0-g$hash').frameworkVersionFor(hash), '10.20.30');
+    expect(GitTagVersion.parse('v1.2.3+hotfix.1-4-g$hash').frameworkVersionFor(hash), '1.2.3+hotfix.2-pre.4');
+    expect(GitTagVersion.parse('v7.2.4+hotfix.8-0-g$hash').frameworkVersionFor(hash), '7.2.4+hotfix.8');
+    expect(testLogger.traceText, '');
+    expect(GitTagVersion.parse('x1.2.3-4-g$hash').frameworkVersionFor(hash), '0.0.0-unknown');
+    expect(GitTagVersion.parse('v1.0.0-unknown-0-g$hash').frameworkVersionFor(hash), '0.0.0-unknown');
+    expect(GitTagVersion.parse('beta-1-g$hash').frameworkVersionFor(hash), '0.0.0-unknown');
+    expect(GitTagVersion.parse('v1.2.3-4-gx$hash').frameworkVersionFor(hash), '0.0.0-unknown');
+    expect(testLogger.statusText, '');
+    expect(testLogger.errorText, '');
+    expect(
+      testLogger.traceText,
+      'Could not interpret results of "git describe": x1.2.3-4-gabcdef\n'
+      'Could not interpret results of "git describe": v1.0.0-unknown-0-gabcdef\n'
+      'Could not interpret results of "git describe": beta-1-gabcdef\n'
+      'Could not interpret results of "git describe": v1.2.3-4-gxabcdef\n',
+    );
+  });
+}
+
+void _expectVersionMessage(String message) {
+  final BufferLogger logger = context.get<Logger>();
+  expect(logger.statusText.trim(), message.trim());
+  logger.clear();
+}
+
+void fakeData(
+  ProcessManager pm,
+  Cache cache, {
+  DateTime localCommitDate,
+  DateTime remoteCommitDate,
+  VersionCheckStamp stamp,
+  String stampJson,
+  bool errorOnFetch = false,
+  bool expectSetStamp = false,
+  bool expectServerPing = false,
+  String channel = 'master',
+}) {
+  ProcessResult success(String standardOutput) {
+    return ProcessResult(1, 0, standardOutput, '');
+  }
+
+  ProcessResult failure(int exitCode) {
+    return ProcessResult(1, exitCode, '', 'error');
+  }
+
+  when(cache.getStampFor(any)).thenAnswer((Invocation invocation) {
+    expect(invocation.positionalArguments.single, VersionCheckStamp.flutterVersionCheckStampFile);
+
+    if (stampJson != null) {
+      return stampJson;
+    }
+
+    if (stamp != null) {
+      return json.encode(stamp.toJson());
+    }
+
+    return null;
+  });
+
+  when(cache.setStampFor(any, any)).thenAnswer((Invocation invocation) {
+    expect(invocation.positionalArguments.first, VersionCheckStamp.flutterVersionCheckStampFile);
+
+    if (expectSetStamp) {
+      stamp = VersionCheckStamp.fromJson(json.decode(invocation.positionalArguments[1]));
+      return null;
+    }
+
+    throw StateError('Unexpected call to Cache.setStampFor(${invocation.positionalArguments}, ${invocation.namedArguments})');
+  });
+
+  final Answering<ProcessResult> syncAnswer = (Invocation invocation) {
+    bool argsAre(String a1, [ String a2, String a3, String a4, String a5, String a6, String a7, String a8 ]) {
+      const ListEquality<String> equality = ListEquality<String>();
+      final List<String> args = invocation.positionalArguments.single;
+      final List<String> expectedArgs = <String>[a1, a2, a3, a4, a5, a6, a7, a8].where((String arg) => arg != null).toList();
+      return equality.equals(args, expectedArgs);
+    }
+
+    if (argsAre('git', 'log', '-n', '1', '--pretty=format:%ad', '--date=iso')) {
+      return success(localCommitDate.toString());
+    } else if (argsAre('git', 'remote')) {
+      return success('');
+    } else if (argsAre('git', 'remote', 'add', '__flutter_version_check__', 'https://github.com/flutter/flutter.git')) {
+      return success('');
+    } else if (argsAre('git', 'fetch', '__flutter_version_check__', channel)) {
+      if (!expectServerPing) {
+        fail('Did not expect server ping');
+      }
+      return errorOnFetch ? failure(128) : success('');
+    } else if (remoteCommitDate != null && argsAre('git', 'log', '__flutter_version_check__/$channel', '-n', '1', '--pretty=format:%ad', '--date=iso')) {
+      return success(remoteCommitDate.toString());
+    }
+
+    throw StateError('Unexpected call to ProcessManager.run(${invocation.positionalArguments}, ${invocation.namedArguments})');
+  };
+
+  when(pm.runSync(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer(syncAnswer);
+  when(pm.run(any, workingDirectory: anyNamed('workingDirectory'))).thenAnswer((Invocation invocation) async {
+    return syncAnswer(invocation);
+  });
+
+  when(pm.runSync(
+    <String>['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{u}'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(101, 0, channel, ''));
+  when(pm.runSync(
+    <String>['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(102, 0, 'branch', ''));
+  when(pm.runSync(
+    <String>['git', 'log', '-n', '1', '--pretty=format:%H'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(103, 0, '1234abcd', ''));
+  when(pm.runSync(
+    <String>['git', 'log', '-n', '1', '--pretty=format:%ar'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(104, 0, '1 second ago', ''));
+  when(pm.runSync(
+    <String>['git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags'],
+    workingDirectory: anyNamed('workingDirectory'),
+    environment: anyNamed('environment'),
+  )).thenReturn(ProcessResult(105, 0, 'v0.1.2-3-1234abcd', ''));
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart
new file mode 100644
index 0000000..ccf816b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart
@@ -0,0 +1,257 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
+import 'package:quiver/testing/async.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+class MockPeer implements rpc.Peer {
+
+  @override
+  rpc.ErrorCallback get onUnhandledError => null;
+
+  @override
+  Future<dynamic> get done async {
+    throw 'unexpected call to done';
+  }
+
+  @override
+  bool get isClosed {
+    throw 'unexpected call to isClosed';
+  }
+
+  @override
+  Future<dynamic> close() async {
+    throw 'unexpected call to close()';
+  }
+
+  @override
+  Future<dynamic> listen() async {
+    // this does get called
+  }
+
+  @override
+  void registerFallback(dynamic callback(rpc.Parameters parameters)) {
+    throw 'unexpected call to registerFallback';
+  }
+
+  @override
+  void registerMethod(String name, Function callback) {
+    // this does get called
+  }
+
+  @override
+  void sendNotification(String method, [ dynamic parameters ]) {
+    throw 'unexpected call to sendNotification';
+  }
+
+  bool isolatesEnabled = false;
+
+  Future<void> _getVMLatch;
+  Completer<void> _currentGetVMLatchCompleter;
+
+  void tripGetVMLatch() {
+    final Completer<void> lastCompleter = _currentGetVMLatchCompleter;
+    _currentGetVMLatchCompleter = Completer<void>();
+    _getVMLatch = _currentGetVMLatchCompleter.future;
+    lastCompleter?.complete();
+  }
+
+  int returnedFromSendRequest = 0;
+
+  @override
+  Future<dynamic> sendRequest(String method, [ dynamic parameters ]) async {
+    if (method == 'getVM')
+      await _getVMLatch;
+    await Future<void>.delayed(Duration.zero);
+    returnedFromSendRequest += 1;
+    if (method == 'getVM') {
+      return <String, dynamic>{
+        'type': 'VM',
+        'name': 'vm',
+        'architectureBits': 64,
+        'targetCPU': 'x64',
+        'hostCPU': '      Intel(R) Xeon(R) CPU    E5-1650 v2 @ 3.50GHz',
+        'version': '2.1.0-dev.7.1.flutter-45f9462398 (Fri Oct 19 19:27:56 2018 +0000) on "linux_x64"',
+        '_profilerMode': 'Dart',
+        '_nativeZoneMemoryUsage': 0,
+        'pid': 103707,
+        'startTime': 1540426121876,
+        '_embedder': 'Flutter',
+        '_maxRSS': 312614912,
+        '_currentRSS': 33091584,
+        'isolates': isolatesEnabled ? <dynamic>[
+          <String, dynamic>{
+            'type': '@Isolate',
+            'fixedId': true,
+            'id': 'isolates/242098474',
+            'name': 'main.dart:main()',
+            'number': 242098474,
+          },
+        ] : <dynamic>[],
+      };
+    }
+    if (method == 'getIsolate') {
+      return <String, dynamic>{
+        'type': 'Isolate',
+        'fixedId': true,
+        'id': 'isolates/242098474',
+        'name': 'main.dart:main()',
+        'number': 242098474,
+        '_originNumber': 242098474,
+        'startTime': 1540488745340,
+        '_heaps': <String, dynamic>{
+          'new': <String, dynamic>{
+            'used': 0,
+            'capacity': 0,
+            'external': 0,
+            'collections': 0,
+            'time': 0.0,
+            'avgCollectionPeriodMillis': 0.0,
+          },
+          'old': <String, dynamic>{
+            'used': 0,
+            'capacity': 0,
+            'external': 0,
+            'collections': 0,
+            'time': 0.0,
+            'avgCollectionPeriodMillis': 0.0,
+          },
+        },
+      };
+    }
+    if (method == '_flutter.listViews') {
+      return <String, dynamic>{
+        'type': 'FlutterViewList',
+        'views': isolatesEnabled ? <dynamic>[
+          <String, dynamic>{
+            'type': 'FlutterView',
+            'id': '_flutterView/0x4a4c1f8',
+            'isolate': <String, dynamic>{
+              'type': '@Isolate',
+              'fixedId': true,
+              'id': 'isolates/242098474',
+              'name': 'main.dart:main()',
+              'number': 242098474,
+            },
+          },
+        ] : <dynamic>[],
+      };
+    }
+    return null;
+  }
+
+  @override
+  dynamic withBatch(dynamic callback()) {
+    throw 'unexpected call to withBatch';
+  }
+}
+
+void main() {
+  MockStdio mockStdio;
+  group('VMService', () {
+    setUp(() {
+      mockStdio = MockStdio();
+    });
+
+    testUsingContext('fails connection eagerly in the connect() method', () async {
+      FakeAsync().run((FakeAsync time) {
+        bool failed = false;
+        final Future<VMService> future = VMService.connect(Uri.parse('http://host.invalid:9999/'));
+        future.whenComplete(() {
+          failed = true;
+        });
+        time.elapse(const Duration(seconds: 5));
+        expect(failed, isFalse);
+        expect(mockStdio.writtenToStdout.join(''), '');
+        expect(mockStdio.writtenToStderr.join(''), '');
+        time.elapse(const Duration(seconds: 5));
+        expect(failed, isFalse);
+        expect(mockStdio.writtenToStdout.join(''), 'This is taking longer than expected...\n');
+        expect(mockStdio.writtenToStderr.join(''), '');
+      });
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      Stdio: () => mockStdio,
+      WebSocketConnector: () => (String url, {CompressionOptions compression}) async => throw const SocketException('test'),
+    });
+
+    testUsingContext('refreshViews', () {
+      FakeAsync().run((FakeAsync time) {
+        bool done = false;
+        final MockPeer mockPeer = MockPeer();
+        expect(mockPeer.returnedFromSendRequest, 0);
+        final VMService vmService = VMService(mockPeer, null, null, null, null, null);
+        vmService.getVM().then((void value) { done = true; });
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 0);
+        time.elapse(Duration.zero);
+        expect(done, isTrue);
+        expect(mockPeer.returnedFromSendRequest, 1);
+
+        done = false;
+        mockPeer.tripGetVMLatch(); // this blocks the upcoming getVM call
+        final Future<void> ready = vmService.refreshViews(waitForViews: true);
+        ready.then((void value) { done = true; });
+        expect(mockPeer.returnedFromSendRequest, 1);
+        time.elapse(Duration.zero); // this unblocks the listViews call which returns nothing
+        expect(mockPeer.returnedFromSendRequest, 2);
+        time.elapse(const Duration(milliseconds: 50)); // the last listViews had no views, so it waits 50ms, then calls getVM
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 2);
+        mockPeer.tripGetVMLatch(); // this unblocks the getVM call
+        expect(mockPeer.returnedFromSendRequest, 2);
+        time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
+        expect(mockPeer.returnedFromSendRequest, 4);
+        time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 4);
+        mockPeer.tripGetVMLatch(); // this unblocks the getVM call
+        expect(mockPeer.returnedFromSendRequest, 4);
+        time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
+        expect(mockPeer.returnedFromSendRequest, 6);
+        time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 6);
+        mockPeer.tripGetVMLatch(); // this unblocks the getVM call
+        expect(mockPeer.returnedFromSendRequest, 6);
+        time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
+        expect(mockPeer.returnedFromSendRequest, 8);
+        time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 8);
+        mockPeer.tripGetVMLatch(); // this unblocks the getVM call
+        expect(mockPeer.returnedFromSendRequest, 8);
+        time.elapse(Duration.zero); // here getVM returns with no isolates and listViews returns no views
+        expect(mockPeer.returnedFromSendRequest, 10);
+        const String message = 'Flutter is taking longer than expected to report its views. Still trying...\n';
+        expect(mockStdio.writtenToStdout.join(''), message);
+        expect(mockStdio.writtenToStderr.join(''), '');
+        time.elapse(const Duration(milliseconds: 50)); // so refreshViews waits another 50ms
+        expect(done, isFalse);
+        expect(mockPeer.returnedFromSendRequest, 10);
+        mockPeer.isolatesEnabled = true;
+        mockPeer.tripGetVMLatch(); // this unblocks the getVM call
+        expect(mockPeer.returnedFromSendRequest, 10);
+        time.elapse(Duration.zero); // now it returns an isolate and the listViews call returns views
+        expect(mockPeer.returnedFromSendRequest, 13);
+        expect(done, isTrue);
+        expect(mockStdio.writtenToStdout.join(''), message);
+        expect(mockStdio.writtenToStderr.join(''), '');
+      });
+    }, overrides: <Type, Generator>{
+      Logger: () => StdoutLogger(),
+      Stdio: () => mockStdio,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart
new file mode 100644
index 0000000..31ca493
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart
@@ -0,0 +1,95 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/web/chrome.dart';
+import 'package:flutter_tools/src/web/web_device.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(ChromeDevice, () {
+   MockChromeLauncher mockChromeLauncher;
+   MockPlatform mockPlatform;
+   MockProcessManager mockProcessManager;
+
+    setUp(() async {
+      mockProcessManager = MockProcessManager();
+      mockChromeLauncher = MockChromeLauncher();
+      mockPlatform = MockPlatform();
+      when(mockChromeLauncher.launch(any)).thenAnswer((Invocation invocation) async {
+        return null;
+      });
+    });
+
+    test('Defaults', () async {
+      final ChromeDevice chromeDevice = ChromeDevice();
+
+      expect(chromeDevice.name, 'Chrome');
+      expect(chromeDevice.id, 'chrome');
+      expect(chromeDevice.supportsHotReload, true);
+      expect(chromeDevice.supportsHotRestart, true);
+      expect(chromeDevice.supportsStartPaused, true);
+      expect(chromeDevice.supportsFlutterExit, true);
+      expect(chromeDevice.supportsScreenshot, false);
+      expect(await chromeDevice.isLocalEmulator, false);
+    });
+
+    testUsingContext('Invokes version command on non-Windows platforms', () async{
+      when(mockPlatform.isWindows).thenReturn(false);
+      when(mockProcessManager.canRun('chrome.foo')).thenReturn(true);
+      when(mockProcessManager.run(<String>['chrome.foo', '--version'])).thenAnswer((Invocation invocation) async {
+        return MockProcessResult(0, 'ABC');
+      });
+      final ChromeDevice chromeDevice = ChromeDevice();
+
+      expect(chromeDevice.isSupported(), true);
+      expect(await chromeDevice.sdkNameAndVersion, 'ABC');
+    }, overrides: <Type, Generator>{
+      Platform: () => mockPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('Invokes different version command on windows.', () async {
+      when(mockPlatform.isWindows).thenReturn(true);
+      when(mockProcessManager.canRun('chrome.foo')).thenReturn(true);
+      when(mockProcessManager.run(<String>[
+        'reg',
+        'query',
+        'HKEY_CURRENT_USER\\Software\\Google\\Chrome\\BLBeacon',
+        '/v',
+        'version',
+      ])).thenAnswer((Invocation invocation) async {
+        return MockProcessResult(0, r'HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon\ version REG_SZ 74.0.0 A');
+      });
+      final ChromeDevice chromeDevice = ChromeDevice();
+
+      expect(chromeDevice.isSupported(), true);
+      expect(await chromeDevice.sdkNameAndVersion, 'Google Chrome 74.0.0');
+    }, overrides: <Type, Generator>{
+      Platform: () => mockPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+}
+
+class MockChromeLauncher extends Mock implements ChromeLauncher {}
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{'FLUTTER_WEB': 'true', kChromeEnvironment: 'chrome.foo'};
+}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcessResult extends Mock implements ProcessResult {
+  MockProcessResult(this.exitCode, this.stdout);
+
+  @override
+  final int exitCode;
+
+  @override
+  final String stdout;
+}
diff --git a/packages/flutter_tools/test/general.shard/web/web_validator_test.dart b/packages/flutter_tools/test/general.shard/web/web_validator_test.dart
new file mode 100644
index 0000000..c95972d
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/web/web_validator_test.dart
@@ -0,0 +1,65 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/web/chrome.dart';
+import 'package:flutter_tools/src/web/web_validator.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  group('WebValidator', () {
+    Testbed testbed;
+    WebValidator webValidator;
+    MockPlatform mockPlatform;
+    MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+      testbed = Testbed(setup: () {
+        when(mockProcessManager.canRun(kMacOSExecutable)).thenReturn(true);
+        return null;
+      }, overrides: <Type, Generator>{
+        Platform: () => mockPlatform,
+        ProcessManager: () => mockProcessManager,
+      });
+      webValidator = const WebValidator();
+      mockPlatform = MockPlatform();
+      when(mockPlatform.isMacOS).thenReturn(true);
+      when(mockPlatform.isWindows).thenReturn(false);
+      when(mockPlatform.isLinux).thenReturn(false);
+    });
+
+    test('Can find macOS executable ', () => testbed.run(() async {
+      final ValidationResult result = await webValidator.validate();
+      expect(result.type, ValidationType.installed);
+    }));
+
+    test('Can notice missing macOS executable ', () => testbed.run(() async {
+      when(mockProcessManager.canRun(kMacOSExecutable)).thenReturn(false);
+      final ValidationResult result = await webValidator.validate();
+      expect(result.type, ValidationType.missing);
+    }));
+
+    test('Doesn\'t warn about CHROME_EXECUTABLE unless it cant find chrome ', () => testbed.run(() async {
+      when(mockProcessManager.canRun(kMacOSExecutable)).thenReturn(false);
+      final ValidationResult result = await webValidator.validate();
+      expect(result.messages, <ValidationMessage>[
+        ValidationMessage.hint('CHROME_EXECUTABLE not set')
+      ]);
+      expect(result.type, ValidationType.missing);
+    }));
+  });
+}
+
+class MockPlatform extends Mock implements Platform  {
+  @override
+  Map<String, String> get environment => const <String, String>{};
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
diff --git a/packages/flutter_tools/test/general.shard/web/workflow_test.dart b/packages/flutter_tools/test/general.shard/web/workflow_test.dart
new file mode 100644
index 0000000..86a8d6b
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/web/workflow_test.dart
@@ -0,0 +1,131 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/version.dart';
+import 'package:flutter_tools/src/web/chrome.dart';
+import 'package:flutter_tools/src/web/workflow.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/testbed.dart';
+
+void main() {
+  group('WebWorkflow', () {
+    Testbed testbed;
+    MockPlatform notSupported;
+    MockPlatform windows;
+    MockPlatform linux;
+    MockPlatform macos;
+    MockProcessManager mockProcessManager;
+    MockFlutterVersion unstable;
+    MockFlutterVersion stable;
+    WebWorkflow workflow;
+
+    setUpAll(() {
+      unstable = MockFlutterVersion(false);
+      stable = MockFlutterVersion(true);
+      notSupported = MockPlatform(linux: false, windows: false, macos: false);
+      windows = MockPlatform(windows: true);
+      linux = MockPlatform(linux: true);
+      macos = MockPlatform(macos: true);
+      workflow = const WebWorkflow();
+      mockProcessManager = MockProcessManager();
+      testbed = Testbed(setup: () async {
+        fs.file('chrome').createSync();
+        when(mockProcessManager.canRun('chrome')).thenReturn(true);
+      }, overrides: <Type, Generator>{
+        FlutterVersion: () => unstable,
+        ProcessManager: () => mockProcessManager,
+      });
+    });
+
+    test('Applies on Linux', () => testbed.run(() {
+      expect(workflow.appliesToHostPlatform, true);
+      expect(workflow.canLaunchDevices, true);
+      expect(workflow.canListDevices, true);
+      expect(workflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => linux,
+    }));
+
+    test('Applies on macOS', () => testbed.run(() {
+      expect(workflow.appliesToHostPlatform, true);
+      expect(workflow.canLaunchDevices, true);
+      expect(workflow.canListDevices, true);
+      expect(workflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => macos,
+    }));
+
+    test('Applies on Windows', () => testbed.run(() {
+      expect(workflow.appliesToHostPlatform, true);
+      expect(workflow.canLaunchDevices, true);
+      expect(workflow.canListDevices, true);
+      expect(workflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => windows,
+    }));
+
+    test('does not apply on other platforms', () => testbed.run(() {
+      when(mockProcessManager.canRun('chrome')).thenReturn(false);
+      expect(workflow.appliesToHostPlatform, false);
+      expect(workflow.canLaunchDevices, false);
+      expect(workflow.canListDevices, false);
+      expect(workflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => notSupported,
+    }));
+
+    test('does not apply on stable branch', () => testbed.run(() {
+      expect(workflow.appliesToHostPlatform, false);
+      expect(workflow.canLaunchDevices, false);
+      expect(workflow.canListDevices, false);
+      expect(workflow.canListEmulators, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => macos,
+      FlutterVersion: () => stable,
+    }));
+  });
+}
+
+class MockFlutterVersion extends Mock implements FlutterVersion {
+  MockFlutterVersion(this.isStable);
+
+  final bool isStable;
+
+  @override
+  bool get isMaster => !isStable;
+}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockPlatform extends Mock implements Platform {
+  MockPlatform(
+      {this.windows = false,
+      this.macos = false,
+      this.linux = false,
+      this.environment = const <String, String>{
+        kChromeEnvironment: 'chrome',
+      }});
+
+  final bool windows;
+  final bool macos;
+  final bool linux;
+
+  @override
+  final Map<String, String> environment;
+
+  @override
+  bool get isLinux => linux;
+
+  @override
+  bool get isMacOS => macos;
+
+  @override
+  bool get isWindows => windows;
+}
diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart
new file mode 100644
index 0000000..8d82bac
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart
@@ -0,0 +1,235 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/convert.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  Map<String, String> environment = <String, String>{};
+}
+class MockProcessManager extends Mock implements ProcessManager {}
+class MockProcessResult extends Mock implements ProcessResult {}
+
+void main() {
+  const String programFilesPath = r'C:\Program Files (x86)';
+  const String visualStudioPath = programFilesPath + r'\Microsoft Visual Studio\2017\Community';
+  const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat';
+  const String vswherePath = programFilesPath + r'\Microsoft Visual Studio\Installer\vswhere.exe';
+
+  final MockPlatform windowsPlatform = MockPlatform()
+      ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
+  MockProcessManager mockProcessManager;
+  final MemoryFileSystem memoryFilesystem = MemoryFileSystem(style: FileSystemStyle.windows);
+
+  // Sets up the mock environment so that searching for Visual Studio with
+  // exactly the given required components will provide a result. By default it
+  // return a preset installation, but the response can be overridden.
+  void setMockVswhereResponse([List<String> requiredComponents, String response]) {
+    fs.file(vswherePath).createSync(recursive: true);
+    fs.file(vcvarsPath).createSync(recursive: true);
+
+    final MockProcessResult result = MockProcessResult();
+    when(result.exitCode).thenReturn(0);
+    when<String>(result.stdout).thenReturn(response ??
+      json.encode(<Map<String, dynamic>>[
+        <String, dynamic>{
+          'installationPath': visualStudioPath,
+          'displayName': 'Visual Studio Community 2017',
+          'installationVersion': '15.9.28307.665',
+          'catalog': <String, String>{
+            'productDisplayVersion': '15.9.12',
+          },
+        },
+      ]));
+
+    final List<String> requirementArguments = requiredComponents == null
+        ? <String>[]
+        : <String>['-requires', ...requiredComponents];
+    when(mockProcessManager.runSync(<String>[
+      vswherePath,
+        '-format', 'json',
+        '-utf8',
+        '-latest',
+        ...?requirementArguments,
+    ])).thenAnswer((Invocation invocation) {
+      return result;
+    });
+  }
+
+  // Sets whether or not a vswhere query without components will return an
+  // installation.
+  void setMockIncompleteVisualStudioExists(bool exists) {
+    setMockVswhereResponse(null, exists ? null : '[]');
+  }
+
+  // Sets whether or not a vswhere query with the required components will
+  // return an installation.
+  void setMockCompatibleVisualStudioExists(bool exists) {
+    setMockVswhereResponse(<String>[
+      'Microsoft.Component.MSBuild',
+      'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
+      'Microsoft.VisualStudio.Component.Windows10SDK.17763',
+    ], exists ? null : '[]');
+  }
+
+  group('Visual Studio', () {
+    VisualStudio visualStudio;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+    });
+
+    testUsingContext('isInstalled returns false when vswhere is missing', () {
+      when(mockProcessManager.runSync(any))
+          .thenThrow(const ProcessException('vswhere', <String>[]));
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('vcvarsPath returns null when vswhere is missing', () {
+      when(mockProcessManager.runSync(any))
+          .thenThrow(const ProcessException('vswhere', <String>[]));
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.vcvarsPath, isNull);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('isInstalled returns false when vswhere returns non-zero', () {
+      when(mockProcessManager.runSync(any))
+          .thenThrow(const ProcessException('vswhere', <String>[]));
+          final MockProcessResult result = MockProcessResult();
+      when(result.exitCode).thenReturn(1);
+      when(mockProcessManager.runSync(any)).thenAnswer((Invocation invocation) {
+        return result;
+      });
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('isInstalled returns true when VS is present but missing components', () {
+      setMockIncompleteVisualStudioExists(true);
+      setMockCompatibleVisualStudioExists(false);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('hasNecessaryComponents returns false when VS is present but missing components', () {
+      setMockIncompleteVisualStudioExists(true);
+      setMockCompatibleVisualStudioExists(false);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.hasNecessaryComponents, false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('vcvarsPath returns null when VS is present but missing components', () {
+      setMockIncompleteVisualStudioExists(true);
+      setMockCompatibleVisualStudioExists(false);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.vcvarsPath, isNull);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('VS metadata is available when VS is present, even if missing components', () {
+      setMockIncompleteVisualStudioExists(true);
+      setMockCompatibleVisualStudioExists(false);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.displayName, equals('Visual Studio Community 2017'));
+      expect(visualStudio.displayVersion, equals('15.9.12'));
+      expect(visualStudio.installLocation, equals(visualStudioPath));
+      expect(visualStudio.fullVersion, equals('15.9.28307.665'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+
+    testUsingContext('isInstalled returns true when VS is present but missing components', () {
+      setMockIncompleteVisualStudioExists(true);
+      setMockCompatibleVisualStudioExists(false);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('Everything returns good values when VS is present with all components', () {
+      setMockCompatibleVisualStudioExists(true);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, true);
+      expect(visualStudio.hasNecessaryComponents, true);
+      expect(visualStudio.vcvarsPath, equals(vcvarsPath));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('Metadata is for compatible version when latest is missing components', () {
+      setMockCompatibleVisualStudioExists(true);
+      // Return a different version for queries without the required packages.
+      final String incompleteVersionResponse = json.encode(<Map<String, dynamic>>[
+          <String, dynamic>{
+            'installationPath': visualStudioPath,
+            'displayName': 'Visual Studio Community 2019',
+            'installationVersion': '16.1.1.1',
+            'catalog': <String, String>{
+              'productDisplayVersion': '16.1',
+            },
+          }
+      ]);
+      setMockVswhereResponse(null, incompleteVersionResponse);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.displayName, equals('Visual Studio Community 2017'));
+      expect(visualStudio.displayVersion, equals('15.9.12'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      Platform: () => windowsPlatform,
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart
new file mode 100644
index 0000000..f3717b1
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart
@@ -0,0 +1,55 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_tools/src/doctor.dart';
+import 'package:flutter_tools/src/windows/visual_studio.dart';
+import 'package:flutter_tools/src/windows/visual_studio_validator.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+class MockVisualStudio extends Mock implements VisualStudio {}
+
+void main() {
+  group('Visual Studio validation', () {
+    MockVisualStudio mockVisualStudio;
+
+    setUp(() {
+      mockVisualStudio = MockVisualStudio();
+    });
+
+    testUsingContext('Emits missing status when Visual Studio is not installed', () async {
+      when(visualStudio.isInstalled).thenReturn(false);
+      const VisualStudioValidator validator = VisualStudioValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.missing);
+    }, overrides: <Type, Generator>{
+      VisualStudio: () => mockVisualStudio,
+    });
+
+    testUsingContext('Emits partial status when Visual Studio is installed without necessary components', () async {
+      when(visualStudio.isInstalled).thenReturn(true);
+      when(visualStudio.hasNecessaryComponents).thenReturn(false);
+      when(visualStudio.workloadDescription).thenReturn('Desktop development');
+      when(visualStudio.necessaryComponentDescriptions(any)).thenReturn(<String>['A', 'B']);
+      when(visualStudio.fullVersion).thenReturn('15.1');
+      const VisualStudioValidator validator = VisualStudioValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      VisualStudio: () => mockVisualStudio,
+    });
+
+    testUsingContext('Emits installed status when Visual Studio is installed with necessary components', () async {
+      when(visualStudio.isInstalled).thenReturn(true);
+      when(visualStudio.hasNecessaryComponents).thenReturn(true);
+      const VisualStudioValidator validator = VisualStudioValidator();
+      final ValidationResult result = await validator.validate();
+      expect(result.type, ValidationType.installed);
+    }, overrides: <Type, Generator>{
+      VisualStudio: () => mockVisualStudio,
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart
new file mode 100644
index 0000000..29fa679
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart
@@ -0,0 +1,98 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/windows/application_package.dart';
+import 'package:flutter_tools/src/windows/windows_device.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:mockito/mockito.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(WindowsDevice, () {
+    final WindowsDevice device = WindowsDevice();
+    final MockPlatform notWindows = MockPlatform();
+    final MockProcessManager mockProcessManager = MockProcessManager();
+
+    when(notWindows.isWindows).thenReturn(false);
+    when(notWindows.environment).thenReturn(const <String, String>{});
+    when(mockProcessManager.runSync(<String>[
+      'powershell', '-script="Get-CimInstance Win32_Process"'
+    ])).thenAnswer((Invocation invocation) {
+      final MockProcessResult result = MockProcessResult();
+      when(result.exitCode).thenReturn(0);
+      when<String>(result.stdout).thenReturn('');
+      return result;
+    });
+
+    testUsingContext('defaults', () async {
+      final PrebuiltWindowsApp windowsApp = PrebuiltWindowsApp(executable: 'foo');
+      expect(await device.targetPlatform, TargetPlatform.windows_x64);
+      expect(device.name, 'Windows');
+      expect(await device.installApp(windowsApp), true);
+      expect(await device.uninstallApp(windowsApp), true);
+      expect(await device.isLatestBuildInstalled(windowsApp), true);
+      expect(await device.isAppInstalled(windowsApp), true);
+      expect(await device.stopApp(windowsApp), false);
+      expect(device.category, Category.desktop);
+    }, overrides: <Type, Generator>{
+      ProcessManager: () => mockProcessManager,
+    });
+
+    test('noop port forwarding', () async {
+      final WindowsDevice device = WindowsDevice();
+      final DevicePortForwarder portForwarder = device.portForwarder;
+      final int result = await portForwarder.forward(2);
+      expect(result, 2);
+      expect(portForwarder.forwardedPorts.isEmpty, true);
+    });
+
+    testUsingContext('No devices listed if platform unsupported', () async {
+      expect(await WindowsDevices().devices, <Device>[]);
+    }, overrides: <Type, Generator>{
+      Platform: () => notWindows,
+    });
+
+    testUsingContext('isSupportedForProject is true with editable host app', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      fs.directory('windows').createSync();
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(WindowsDevice().isSupportedForProject(flutterProject), true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
+
+    testUsingContext('isSupportedForProject is false with no host app', () async {
+      fs.file('pubspec.yaml').createSync();
+      fs.file('.packages').createSync();
+      final FlutterProject flutterProject = FlutterProject.current();
+
+      expect(WindowsDevice().isSupportedForProject(flutterProject), false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
+  });
+}
+
+class MockPlatform extends Mock implements Platform {}
+
+class MockFileSystem extends Mock implements FileSystem {}
+
+class MockFile extends Mock implements File {}
+
+class MockProcessManager extends Mock implements ProcessManager {}
+
+class MockProcess extends Mock implements Process {}
+
+class MockProcessResult extends Mock implements ProcessResult {}
diff --git a/packages/flutter_tools/test/general.shard/windows/windows_workflow_test.dart b/packages/flutter_tools/test/general.shard/windows/windows_workflow_test.dart
new file mode 100644
index 0000000..4c9d202
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/windows/windows_workflow_test.dart
@@ -0,0 +1,47 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:mockito/mockito.dart';
+
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/windows/windows_workflow.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  group(WindowsWorkflow, () {
+    final MockPlatform windows = MockPlatform();
+    final MockPlatform windowsWithFde = MockPlatform()
+      ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
+    final MockPlatform notWindows = MockPlatform();
+    when(windows.isWindows).thenReturn(true);
+    when(windowsWithFde.isWindows).thenReturn(true);
+    when(notWindows.isWindows).thenReturn(false);
+
+    testUsingContext('Applies to windows platform', () {
+      expect(windowsWorkflow.appliesToHostPlatform, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => windows,
+    });
+    testUsingContext('Does not apply to non-windows platform', () {
+      expect(windowsWorkflow.appliesToHostPlatform, false);
+    }, overrides: <Type, Generator>{
+      Platform: () => notWindows,
+    });
+
+    testUsingContext('defaults', () {
+      expect(windowsWorkflow.canListEmulators, false);
+      expect(windowsWorkflow.canLaunchDevices, true);
+      expect(windowsWorkflow.canListDevices, true);
+    }, overrides: <Type, Generator>{
+      Platform: () => windowsWithFde,
+    });
+  });
+}
+
+class MockPlatform extends Mock implements Platform {
+  @override
+  final Map<String, String> environment = <String, String>{};
+}