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>{};
+}