project_test.dart 29.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6

7 8
import 'package:file/file.dart';
import 'package:file/memory.dart';
9
import 'package:flutter_tools/src/base/context.dart';
10
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/features.dart';
13
import 'package:flutter_tools/src/flutter_manifest.dart';
14
import 'package:flutter_tools/src/ios/plist_parser.dart';
15
import 'package:flutter_tools/src/ios/xcodeproj.dart';
16
import 'package:flutter_tools/src/project.dart';
17
import 'package:flutter_tools/src/globals.dart' as globals;
18
import 'package:meta/meta.dart';
19
import 'package:mockito/mockito.dart';
20

21 22 23
import '../src/common.dart';
import '../src/context.dart';
import '../src/testbed.dart';
24 25 26

void main() {
  group('Project', () {
27 28
    group('construction', () {
      testInMemory('fails on null directory', () async {
29 30
        expect(
          () => FlutterProject.fromDirectory(null),
Dan Field's avatar
Dan Field committed
31
          throwsAssertionError,
32 33 34 35
        );
      });

      testInMemory('fails on invalid pubspec.yaml', () async {
36
        final Directory directory = globals.fs.directory('myproject');
37 38 39
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
40 41 42

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
43
          throwsToolExit(),
44 45 46 47
        );
      });

      testInMemory('fails on pubspec.yaml parse failure', () async {
48
        final Directory directory = globals.fs.directory('myproject');
49 50 51 52 53 54
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
55
          throwsToolExit(),
56 57 58 59
        );
      });

      testInMemory('fails on invalid example/pubspec.yaml', () async {
60
        final Directory directory = globals.fs.directory('myproject');
61 62 63
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
64 65 66

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
67
          throwsToolExit(),
68 69 70 71
        );
      });

      testInMemory('treats missing pubspec.yaml as empty', () async {
72
        final Directory directory = globals.fs.directory('myproject')
73
          ..createSync(recursive: true);
74
        expect((FlutterProject.fromDirectory(directory)).manifest.isEmpty,
75 76 77 78 79
          true,
        );
      });

      testInMemory('reads valid pubspec.yaml', () async {
80
        final Directory directory = globals.fs.directory('myproject');
81 82 83 84
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspec);
        expect(
85
          FlutterProject.fromDirectory(directory).manifest.appName,
86 87 88 89 90
          'hello',
        );
      });

      testInMemory('sets up location', () async {
91
        final Directory directory = globals.fs.directory('myproject');
92
        expect(
93
          FlutterProject.fromDirectory(directory).directory.absolute.path,
94 95 96
          directory.absolute.path,
        );
        expect(
97
          FlutterProject.fromPath(directory.path).directory.absolute.path,
98 99 100
          directory.absolute.path,
        );
        expect(
101
          FlutterProject.current().directory.absolute.path,
102
          globals.fs.currentDirectory.absolute.path,
103 104
        );
      });
105
    });
106

107
    group('editable Android host app', () {
108
      testInMemory('fails on non-module', () async {
109 110
        final FlutterProject project = await someProject();
        await expectLater(
111
          project.android.makeHostAppEditable(),
Dan Field's avatar
Dan Field committed
112
          throwsAssertionError,
113 114
        );
      });
115 116
      testInMemory('exits on already editable module', () async {
        final FlutterProject project = await aModuleProject();
117 118
        await project.android.makeHostAppEditable();
        return expectToolExitLater(project.android.makeHostAppEditable(), contains('already editable'));
119 120
      });
      testInMemory('creates android/app folder in place of .android/app', () async {
121
        final FlutterProject project = await aModuleProject();
122
        await project.android.makeHostAppEditable();
123 124 125 126 127 128 129 130 131 132 133 134 135
        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 {
136
        final FlutterProject project = await aModuleProject();
137
        await project.android.makeHostAppEditable();
138 139 140
        expectExists(project.directory.childDirectory('.android').childDirectory('Flutter'));
        expect(
          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
141
          contains('new File(settingsDir.parentFile, \'.android/include_flutter.groovy\')'),
142 143 144
        );
      });
      testInMemory('can be redone after deletion', () async {
145
        final FlutterProject project = await aModuleProject();
146
        await project.android.makeHostAppEditable();
147
        project.directory.childDirectory('android').deleteSync(recursive: true);
148
        await project.android.makeHostAppEditable();
149 150
        expectExists(project.directory.childDirectory('android').childDirectory('app'));
      });
151
    });
152

153 154
    group('ensure ready for platform-specific tooling', () {
      testInMemory('does nothing, if project is not created', () async {
155
        final FlutterProject project = FlutterProject(
156
          globals.fs.directory('not_created'),
157 158 159
          FlutterManifest.empty(),
          FlutterManifest.empty(),
        );
160
        await project.ensureReadyForPlatformSpecificTooling();
161
        expectNotExists(project.directory);
162
      });
163
      testInMemory('does nothing in plugin or package root project', () async {
164
        final FlutterProject project = await aPluginProject();
165
        await project.ensureReadyForPlatformSpecificTooling();
166
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
167
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
168
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
169
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
170
      });
171
      testInMemory('injects plugins for iOS', () async {
172
        final FlutterProject project = await someProject();
173
        await project.ensureReadyForPlatformSpecificTooling();
174
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
175
      });
176
      testInMemory('generates Xcode configuration for iOS', () async {
177
        final FlutterProject project = await someProject();
178
        await project.ensureReadyForPlatformSpecificTooling();
179
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
180
      });
181
      testInMemory('injects plugins for Android', () async {
182
        final FlutterProject project = await someProject();
183
        await project.ensureReadyForPlatformSpecificTooling();
184
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
185 186
      });
      testInMemory('updates local properties for Android', () async {
187
        final FlutterProject project = await someProject();
188
        await project.ensureReadyForPlatformSpecificTooling();
189
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
190
      });
191 192 193 194 195 196
      testUsingContext('injects plugins for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'));
      }, overrides: <Type, Generator>{
197
        FileSystem: () => MemoryFileSystem(),
198
        ProcessManager: () => FakeProcessManager.any(),
199 200
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(),
201 202 203 204 205 206 207
      });
      testUsingContext('generates Xcode configuration for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.macos.generatedXcodePropertiesFile);
      }, overrides: <Type, Generator>{
208
        FileSystem: () => MemoryFileSystem(),
209
        ProcessManager: () => FakeProcessManager.any(),
210 211
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(),
212 213 214 215 216 217 218 219
      });
      testUsingContext('injects plugins for Linux', () async {
        final FlutterProject project = await someProject();
        project.linux.managedDirectory.createSync(recursive: true);
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
220
        FileSystem: () => MemoryFileSystem(),
221
        ProcessManager: () => FakeProcessManager.any(),
222 223
        FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(),
224 225 226 227 228 229 230 231
      });
      testUsingContext('injects plugins for Windows', () async {
        final FlutterProject project = await someProject();
        project.windows.managedDirectory.createSync(recursive: true);
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
232
        FileSystem: () => MemoryFileSystem(),
233
        ProcessManager: () => FakeProcessManager.any(),
234 235
        FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
        FlutterProjectFactory: () => FlutterProjectFactory(),
236
      });
237 238
      testInMemory('creates Android library in module', () async {
        final FlutterProject project = await aModuleProject();
239
        await project.ensureReadyForPlatformSpecificTooling();
240 241 242
        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
243
      });
244 245
      testInMemory('creates iOS pod in module', () async {
        final FlutterProject project = await aModuleProject();
246
        await project.ensureReadyForPlatformSpecificTooling();
247
        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
248
        expectExists(flutter.childFile('podhelper.rb'));
249 250
        expectExists(flutter.childFile('flutter_export_environment.sh'));
        expectExists(flutter.childFile('${project.manifest.appName}.podspec'));
251 252 253 254 255 256
        expectExists(flutter.childFile('Generated.xcconfig'));
        final Directory pluginRegistrantClasses = flutter
            .childDirectory('FlutterPluginRegistrant')
            .childDirectory('Classes');
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
257 258 259
      });
    });

260 261 262 263 264 265
    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);
266
        expect(project.android.hostAppGradleRoot.basename, '.android');
267
        expect(project.ios.hostAppRoot.basename, '.ios');
268
      });
269
      testInMemory('is known for non-module', () async {
270
        final FlutterProject project = await someProject();
271 272 273
        expect(project.isModule, isFalse);
        expect(project.android.isModule, isFalse);
        expect(project.ios.isModule, isFalse);
274
        expect(project.android.hostAppGradleRoot.basename, 'android');
275
        expect(project.ios.hostAppRoot.basename, 'ios');
276
      });
277
    });
278 279

    group('example', () {
280
      testInMemory('exists for plugin in legacy format', () async {
281
        final FlutterProject project = await aPluginProject();
282 283
        expect(project.hasExampleApp, isTrue);
      });
284 285 286 287
      testInMemory('exists for plugin in multi-platform format', () async {
        final FlutterProject project = await aPluginProject(legacy: false);
        expect(project.hasExampleApp, isTrue);
      });
288
      testInMemory('does not exist for non-plugin', () async {
289
        final FlutterProject project = await someProject();
290 291 292 293
        expect(project.hasExampleApp, isFalse);
      });
    });

294 295 296
    group('language', () {
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
      MemoryFileSystem fs;
297
      FlutterProjectFactory flutterProjectFactory;
298 299 300
      setUp(() {
        fs = MemoryFileSystem();
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
301
        flutterProjectFactory = FlutterProjectFactory();
302 303 304 305 306 307 308
      });

      testInMemory('default host app language', () async {
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

309
      testUsingContext('kotlin host app language', () async {
310 311 312 313
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
314
            return '''
315 316 317 318 319 320
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
321
        FileSystem: () => fs,
322
        ProcessManager: () => FakeProcessManager.any(),
323 324
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
325 326 327
      });
    });

328 329
    group('product bundle identifier', () {
      MemoryFileSystem fs;
330
      MockPlistUtils mockPlistUtils;
331
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
332
      FlutterProjectFactory flutterProjectFactory;
333
      setUp(() {
334
        fs = MemoryFileSystem();
335
        mockPlistUtils = MockPlistUtils();
336
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
337
        flutterProjectFactory = FlutterProjectFactory();
338 339
      });

340
      void testWithMocks(String description, Future<void> testMethod()) {
341 342
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
343
          ProcessManager: () => FakeProcessManager.any(),
344
          PlistParser: () => mockPlistUtils,
345
          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
346
          FlutterProjectFactory: () => flutterProjectFactory,
347 348 349
        });
      }

350
      testWithMocks('null, if no build settings or plist entries', () async {
351
        final FlutterProject project = await someProject();
352
        expect(await project.ios.productBundleIdentifier, isNull);
353
      });
354 355 356 357 358 359 360 361 362 363 364 365 366 367

      testWithMocks('from build settings, if no plist', () async {
        final FlutterProject project = await someProject();
        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
                (_) {
              return Future<Map<String,String>>.value(<String, String>{
                'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              });
            }
        );
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
      });

      testWithMocks('from project file, if no plist or build settings', () async {
368
        final FlutterProject project = await someProject();
369 370 371
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
372
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
373
      });
374

375 376
      testWithMocks('from plist, if no variables', () async {
        final FlutterProject project = await someProject();
377
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
378
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject');
379
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
380
      });
381 382

      testWithMocks('from build settings and plist, if default variable', () async {
383
        final FlutterProject project = await someProject();
384 385 386 387 388 389 390
        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
                (_) {
              return Future<Map<String,String>>.value(<String, String>{
                'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              });
            }
        );
391
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
392
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
393
      });
394 395

      testWithMocks('from build settings and plist, by substitution', () async {
396
        final FlutterProject project = await someProject();
397
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
398 399 400 401 402 403 404 405
        when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
          (_) {
            return Future<Map<String,String>>.value(<String, String>{
              'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              'SUFFIX': 'suffix',
            });
          }
        );
406
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER).\$(SUFFIX)');
407
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject.suffix');
408
      });
409 410
      testWithMocks('empty surrounded by quotes', () async {
        final FlutterProject project = await someProject();
411 412 413
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('', qualifier: '"');
        });
414
        expect(await project.ios.productBundleIdentifier, '');
415 416 417
      });
      testWithMocks('surrounded by double quotes', () async {
        final FlutterProject project = await someProject();
418 419 420
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
        });
421
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
422 423 424
      });
      testWithMocks('surrounded by single quotes', () async {
        final FlutterProject project = await someProject();
425 426 427
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
        });
428
        expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
429
      });
430 431
    });

432 433
    group('organization names set', () {
      testInMemory('is empty, if project not created', () async {
434
        final FlutterProject project = await someProject();
435
        expect(await project.organizationNames, isEmpty);
436 437
      });
      testInMemory('is empty, if no platform folders exist', () async {
438
        final FlutterProject project = await someProject();
439
        project.directory.createSync();
440
        expect(await project.organizationNames, isEmpty);
441 442
      });
      testInMemory('is populated from iOS bundle identifier', () async {
443
        final FlutterProject project = await someProject();
444 445 446
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
        });
447
        expect(await project.organizationNames, <String>['io.flutter']);
448 449
      });
      testInMemory('is populated from Android application ID', () async {
450
        final FlutterProject project = await someProject();
451 452 453 454
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
455
        expect(await project.organizationNames, <String>['io.flutter']);
456 457
      });
      testInMemory('is populated from iOS bundle identifier in plugin example', () async {
458
        final FlutterProject project = await someProject();
459 460 461
        addIosProjectFile(project.example.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '\'');
        });
462
        expect(await project.organizationNames, <String>['io.flutter']);
463 464
      });
      testInMemory('is populated from Android application ID in plugin example', () async {
465
        final FlutterProject project = await someProject();
466 467 468 469
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
470
        expect(await project.organizationNames, <String>['io.flutter']);
471 472
      });
      testInMemory('is populated from Android group in plugin', () async {
473
        final FlutterProject project = await someProject();
474
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
475
        expect(await project.organizationNames, <String>['io.flutter']);
476 477
      });
      testInMemory('is singleton, if sources agree', () async {
478
        final FlutterProject project = await someProject();
479 480 481 482 483 484 485
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
486
        expect(await project.organizationNames, <String>['io.flutter']);
487 488
      });
      testInMemory('is non-singleton, if sources disagree', () async {
489
        final FlutterProject project = await someProject();
490 491 492 493 494 495 496
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
497
        expect(
498
          await project.organizationNames,
499 500 501 502 503
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
504 505 506

  group('Regression test for invalid pubspec', () {
    Testbed testbed;
507
    FlutterProjectFactory flutterProjectFactory;
508 509 510

    setUp(() {
      testbed = Testbed();
511
      flutterProjectFactory = FlutterProjectFactory();
512 513 514
    });

    test('Handles asking for builders from an invalid pubspec', () => testbed.run(() {
515
      globals.fs.file('pubspec.yaml')
516 517 518 519 520 521 522
        ..createSync()
        ..writeAsStringSync(r'''
# Hello, World
''');
      final FlutterProject flutterProject = FlutterProject.current();

      expect(flutterProject.builders, null);
523 524
    }, overrides: <Type, Generator>{
      FlutterProjectFactory: () => flutterProjectFactory,
525 526 527
    }));

    test('Handles asking for builders from a trivial pubspec', () => testbed.run(() {
528
      globals.fs.file('pubspec.yaml')
529 530 531 532 533 534 535 536
        ..createSync()
        ..writeAsStringSync(r'''
# Hello, World
name: foo_bar
''');
      final FlutterProject flutterProject = FlutterProject.current();

      expect(flutterProject.builders, null);
537 538
    }, overrides: <Type, Generator>{
      FlutterProjectFactory: () => flutterProjectFactory,
539 540
    }));
  });
541 542
}

543
Future<FlutterProject> someProject() async {
544
  final Directory directory = globals.fs.directory('some_project');
545 546
  directory.childFile('.packages').createSync(recursive: true);
  directory.childDirectory('ios').createSync(recursive: true);
547 548 549 550 551 552
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
    .writeAsStringSync('<manifest></manifest>');
553
  return FlutterProject.fromDirectory(directory);
554 555
}

556
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
557
  final Directory directory = globals.fs.directory('plugin_project');
558
  directory.childDirectory('ios').createSync(recursive: true);
559 560
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
561 562 563
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
564 565 566 567 568 569
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
570 571 572 573 574 575 576 577 578 579 580 581
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
582 583
      linux:
        pluginClass: MyPlugin
584 585
      macos:
        pluginClass: MyPlugin
586 587
      windows:
        pluginClass: MyPlugin
588 589 590
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
591
  return FlutterProject.fromDirectory(directory);
592 593
}

594
Future<FlutterProject> aModuleProject() async {
595
  final Directory directory = globals.fs.directory('module_project');
596
  directory.childFile('.packages').createSync(recursive: true);
597
  directory.childFile('pubspec.yaml').writeAsStringSync('''
598
name: my_module
599
flutter:
600
  module:
601 602 603
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
604
}
605

606 607
/// Executes the [testMethod] in a context where the file system
/// is in memory.
608
@isTest
609
void testInMemory(String description, Future<void> testMethod()) {
610
  Cache.flutterRoot = getFlutterRoot();
611
  final FileSystem testFileSystem = MemoryFileSystem(
612
    style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
613 614 615
  );
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
616
  transfer(Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem);
617
  transfer(globals.fs.directory(Cache.flutterRoot)
618 619
      .childDirectory('packages')
      .childDirectory('flutter_tools')
620
      .childDirectory('templates'), testFileSystem);
621
  transfer(globals.fs.directory(Cache.flutterRoot)
622 623
      .childDirectory('packages')
      .childDirectory('flutter_tools')
624
      .childDirectory('schema'), testFileSystem);
625 626 627

  final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory();

628 629 630 631
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
632
      FileSystem: () => testFileSystem,
633
      ProcessManager: () => FakeProcessManager.any(),
634
      Cache: () => Cache(),
635
      FlutterProjectFactory: () => flutterProjectFactory,
636 637 638 639
    },
  );
}

640 641 642 643 644
/// 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);
645
    for (final FileSystemEntity child in entity.listSync()) {
646 647 648 649 650 651 652 653 654
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
    throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
  }
}

655 656 657 658 659 660 661 662
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

void expectNotExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isFalse);
}

663
void addIosProjectFile(Directory directory, {String projectFileContent()}) {
664 665 666 667 668
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
669
    ..writeAsStringSync(projectFileContent());
670 671
}

672
void addAndroidGradleFile(Directory directory, { String gradleFileContent() }) {
673 674 675 676 677
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
        ..createSync(recursive: true)
678
        ..writeAsStringSync(gradleFileContent());
679 680 681 682 683 684 685 686
}

void addAndroidWithGroup(Directory directory, String id) {
  directory.childDirectory('android').childFile('build.gradle')
    ..createSync(recursive: true)
    ..writeAsStringSync(gradleFileWithGroupId(id));
}

687 688 689 690 691 692 693 694 695 696 697
String get validPubspec => '''
name: hello
flutter:
''';

String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

698 699 700 701 702 703 704 705
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

706
String projectFileWithBundleId(String id, {String qualifier}) {
707 708 709 710 711
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
712
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
713 714 715 716 717 718 719 720 721 722 723
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
724
    compileSdkVersion 28
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740

    defaultConfig {
        applicationId '$id'
    }
}
''';
}

String gradleFileWithGroupId(String id) {
  return '''
group '$id'
version '1.0-SNAPSHOT'

apply plugin: 'com.android.library'

android {
741
    compileSdkVersion 28
742 743 744
}
''';
}
745 746 747 748 749 750 751 752 753 754

File androidPluginRegistrant(Directory parent) {
  return parent.childDirectory('src')
    .childDirectory('main')
    .childDirectory('java')
    .childDirectory('io')
    .childDirectory('flutter')
    .childDirectory('plugins')
    .childFile('GeneratedPluginRegistrant.java');
}
755

756
class MockPlistUtils extends Mock implements PlistParser {}
757 758 759 760 761

class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
  @override
  bool get isInstalled => true;
}