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

5 6
// @dart = 2.8

7 8 9
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
10
import 'package:flutter_tools/src/base/logger.dart';
11
import 'package:flutter_tools/src/base/os.dart';
12
import 'package:flutter_tools/src/build_info.dart';
13
import 'package:flutter_tools/src/cache.dart';
14
import 'package:flutter_tools/src/convert.dart';
15
import 'package:flutter_tools/src/features.dart';
16
import 'package:flutter_tools/src/flutter_manifest.dart';
17
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
18
import 'package:flutter_tools/src/ios/plist_parser.dart';
19
import 'package:flutter_tools/src/ios/xcodeproj.dart';
20
import 'package:flutter_tools/src/project.dart';
21
import 'package:meta/meta.dart';
22
import 'package:test/fake.dart';
23

24 25
import '../src/common.dart';
import '../src/context.dart';
26
import '../src/fakes.dart';
27 28

void main() {
29
  // TODO(zanderso): remove once FlutterProject is fully refactored.
30 31 32
  // this is safe since no tests have expectations on the test logger.
  final BufferLogger logger = BufferLogger.test();

33
  group('Project', () {
34
    group('construction', () {
35
      _testInMemory('fails on null directory', () async {
36 37
        expect(
          () => FlutterProject.fromDirectory(null),
Dan Field's avatar
Dan Field committed
38
          throwsAssertionError,
39 40 41
        );
      });

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
      testWithoutContext('invalid utf8 throws a tool exit', () {
        final FileSystem fileSystem = MemoryFileSystem.test();
        final FlutterProjectFactory projectFactory = FlutterProjectFactory(
          fileSystem: fileSystem,
          logger: BufferLogger.test(),
        );
        fileSystem.file('pubspec.yaml').writeAsBytesSync(<int>[0xFFFE]);

        /// Technically this should throw a FileSystemException but this is
        /// currently a bug in package:file.
        expect(
          () => projectFactory.fromDirectory(fileSystem.currentDirectory),
          throwsToolExit(),
        );
      });

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

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

70
      _testInMemory('fails on pubspec.yaml parse failure', () async {
71
        final Directory directory = globals.fs.directory('myproject');
72 73 74 75 76 77
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
78
          throwsToolExit(),
79 80 81
        );
      });

82
      _testInMemory('fails on invalid example/pubspec.yaml', () async {
83
        final Directory directory = globals.fs.directory('myproject');
84 85 86
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
87 88 89

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
90
          throwsToolExit(),
91 92 93
        );
      });

94
      _testInMemory('treats missing pubspec.yaml as empty', () async {
95
        final Directory directory = globals.fs.directory('myproject')
96
          ..createSync(recursive: true);
97
        expect(FlutterProject.fromDirectory(directory).manifest.isEmpty,
98 99 100 101
          true,
        );
      });

102
      _testInMemory('reads valid pubspec.yaml', () async {
103
        final Directory directory = globals.fs.directory('myproject');
104 105 106 107
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspec);
        expect(
108
          FlutterProject.fromDirectory(directory).manifest.appName,
109 110 111 112
          'hello',
        );
      });

113 114 115 116 117 118 119 120 121 122 123
      _testInMemory('reads dependencies from pubspec.yaml', () async {
        final Directory directory = globals.fs.directory('myproject');
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(validPubspecWithDependencies);
        expect(
          FlutterProject.fromDirectory(directory).manifest.dependencies,
          <String>{'plugin_a', 'plugin_b'},
        );
      });

124
      _testInMemory('sets up location', () async {
125
        final Directory directory = globals.fs.directory('myproject');
126
        expect(
127
          FlutterProject.fromDirectory(directory).directory.absolute.path,
128 129 130
          directory.absolute.path,
        );
        expect(
131
          FlutterProject.fromDirectoryTest(directory).directory.absolute.path,
132 133 134
          directory.absolute.path,
        );
        expect(
135
          FlutterProject.current().directory.absolute.path,
136
          globals.fs.currentDirectory.absolute.path,
137 138
        );
      });
139
    });
140

141
    group('ensure ready for platform-specific tooling', () {
142
      _testInMemory('does nothing, if project is not created', () async {
143
        final FlutterProject project = FlutterProject(
144
          globals.fs.directory('not_created'),
145 146
          FlutterManifest.empty(logger: logger),
          FlutterManifest.empty(logger: logger),
147
        );
148
        await project.regeneratePlatformSpecificTooling();
149
        expectNotExists(project.directory);
150
      });
151
      _testInMemory('does nothing in plugin or package root project', () async {
152
        final FlutterProject project = await aPluginProject();
153
        await project.regeneratePlatformSpecificTooling();
154
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
155
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
156
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
157
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
158
      });
159
      _testInMemory('injects plugins for iOS', () async {
160
        final FlutterProject project = await someProject();
161
        await project.regeneratePlatformSpecificTooling();
162
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
163
      });
164
      _testInMemory('generates Xcode configuration for iOS', () async {
165
        final FlutterProject project = await someProject();
166
        await project.regeneratePlatformSpecificTooling();
167
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
168
      });
169
      _testInMemory('injects plugins for Android', () async {
170
        final FlutterProject project = await someProject();
171
        await project.regeneratePlatformSpecificTooling();
172
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
173
      });
174
      _testInMemory('updates local properties for Android', () async {
175
        final FlutterProject project = await someProject();
176
        await project.regeneratePlatformSpecificTooling();
177
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
178
      });
179 180 181 182 183 184
      _testInMemory('Android project not on v2 embedding shows a warning', () async {
        final FlutterProject project = await someProject();
        // The default someProject with an empty <manifest> already indicates
        // v1 embedding, as opposed to having <meta-data
        // android:name="flutterEmbedding" android:value="2" />.

185
        await project.regeneratePlatformSpecificTooling();
186 187
        expect(testLogger.statusText, contains('https://flutter.dev/go/android-project-migration'));
      });
188 189 190 191 192 193 194
      _testInMemory('Android plugin without example app does not show a warning', () async {
        final FlutterProject project = await aPluginProject();
        project.example.directory.deleteSync();

        await project.regeneratePlatformSpecificTooling();
        expect(testLogger.statusText, isNot(contains('https://flutter.dev/go/android-project-migration')));
      });
195 196
      _testInMemory('updates local properties for Android', () async {
        final FlutterProject project = await someProject();
197
        await project.regeneratePlatformSpecificTooling();
198 199
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
200 201 202
      testUsingContext('injects plugins for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
203
        await project.regeneratePlatformSpecificTooling();
204 205
        expectExists(project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'));
      }, overrides: <Type, Generator>{
206
        FileSystem: () => MemoryFileSystem.test(),
207
        ProcessManager: () => FakeProcessManager.any(),
208
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
209 210 211 212
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
213 214 215 216
      });
      testUsingContext('generates Xcode configuration for macOS', () async {
        final FlutterProject project = await someProject();
        project.macos.managedDirectory.createSync(recursive: true);
217
        await project.regeneratePlatformSpecificTooling();
218 219
        expectExists(project.macos.generatedXcodePropertiesFile);
      }, overrides: <Type, Generator>{
220
        FileSystem: () => MemoryFileSystem.test(),
221
        ProcessManager: () => FakeProcessManager.any(),
222
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
223 224 225 226
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
227 228 229
      });
      testUsingContext('injects plugins for Linux', () async {
        final FlutterProject project = await someProject();
230
        project.linux.cmakeFile.createSync(recursive: true);
231
        await project.regeneratePlatformSpecificTooling();
232 233 234
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
235
        FileSystem: () => MemoryFileSystem.test(),
236
        ProcessManager: () => FakeProcessManager.any(),
237
        FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
238 239 240 241
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
242 243 244
      });
      testUsingContext('injects plugins for Windows', () async {
        final FlutterProject project = await someProject();
245
        project.windows.cmakeFile.createSync(recursive: true);
246
        await project.regeneratePlatformSpecificTooling();
247 248 249
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
250
        FileSystem: () => MemoryFileSystem.test(),
251
        ProcessManager: () => FakeProcessManager.any(),
252
        FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
253 254 255 256
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
257
      });
258
      _testInMemory('creates Android library in module', () async {
259
        final FlutterProject project = await aModuleProject();
260
        await project.regeneratePlatformSpecificTooling();
261 262 263
        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
264
      });
265
      _testInMemory('creates iOS pod in module', () async {
266
        final FlutterProject project = await aModuleProject();
267
        await project.regeneratePlatformSpecificTooling();
268
        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
269
        expectExists(flutter.childFile('podhelper.rb'));
270 271
        expectExists(flutter.childFile('flutter_export_environment.sh'));
        expectExists(flutter.childFile('${project.manifest.appName}.podspec'));
272 273 274 275 276 277
        expectExists(flutter.childFile('Generated.xcconfig'));
        final Directory pluginRegistrantClasses = flutter
            .childDirectory('FlutterPluginRegistrant')
            .childDirectory('Classes');
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
278
      });
279

280
      testUsingContext('Version.json info is correct', () {
281 282 283 284 285 286
        final MemoryFileSystem fileSystem = MemoryFileSystem.test();
        final FlutterManifest manifest = FlutterManifest.createFromString('''
    name: test
    version: 1.0.0+3
    ''', logger: BufferLogger.test());
        final FlutterProject project = FlutterProject(fileSystem.systemTempDirectory,manifest,manifest);
287
        final Map<String, dynamic> versionInfo = jsonDecode(project.getVersionInfo()) as Map<String, dynamic>;
288 289 290 291
        expect(versionInfo['app_name'],'test');
        expect(versionInfo['version'],'1.0.0');
        expect(versionInfo['build_number'],'3');
      });
292 293
    });

294
    group('module status', () {
295
      _testInMemory('is known for module', () async {
296 297 298 299
        final FlutterProject project = await aModuleProject();
        expect(project.isModule, isTrue);
        expect(project.android.isModule, isTrue);
        expect(project.ios.isModule, isTrue);
300
        expect(project.android.hostAppGradleRoot.basename, '.android');
301
        expect(project.ios.hostAppRoot.basename, '.ios');
302
      });
303
      _testInMemory('is known for non-module', () async {
304
        final FlutterProject project = await someProject();
305 306 307
        expect(project.isModule, isFalse);
        expect(project.android.isModule, isFalse);
        expect(project.ios.isModule, isFalse);
308
        expect(project.android.hostAppGradleRoot.basename, 'android');
309
        expect(project.ios.hostAppRoot.basename, 'ios');
310
      });
311
    });
312 313

    group('example', () {
314
      _testInMemory('exists for plugin in legacy format', () async {
315
        final FlutterProject project = await aPluginProject();
316
        expect(project.isPlugin, isTrue);
317 318
        expect(project.hasExampleApp, isTrue);
      });
319
      _testInMemory('exists for plugin in multi-platform format', () async {
320 321 322
        final FlutterProject project = await aPluginProject(legacy: false);
        expect(project.hasExampleApp, isTrue);
      });
323
      _testInMemory('does not exist for non-plugin', () async {
324
        final FlutterProject project = await someProject();
325
        expect(project.isPlugin, isFalse);
326 327 328 329
        expect(project.hasExampleApp, isFalse);
      });
    });

330
    group('language', () {
331
      XcodeProjectInterpreter xcodeProjectInterpreter;
332
      MemoryFileSystem fs;
333
      FlutterProjectFactory flutterProjectFactory;
334
      setUp(() {
335
        fs = MemoryFileSystem.test();
336
        xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
337 338 339 340
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
341 342
      });

343
      _testInMemory('default host app language', () async {
344 345 346 347
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

348
      testUsingContext('kotlin host app language', () async {
349 350 351 352
        final FlutterProject project = await someProject();

        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
353
            return '''
354 355 356 357 358 359
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
        });
        expect(project.android.isKotlin, isTrue);
      }, overrides: <Type, Generator>{
360
        FileSystem: () => fs,
361
        ProcessManager: () => FakeProcessManager.any(),
362
        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
363
        FlutterProjectFactory: () => flutterProjectFactory,
364 365 366
      });
    });

367 368
    group('product bundle identifier', () {
      MemoryFileSystem fs;
369
      FakePlistParser testPlistUtils;
370
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
371
      FlutterProjectFactory flutterProjectFactory;
372
      setUp(() {
373
        fs = MemoryFileSystem.test();
374
        testPlistUtils = FakePlistParser();
375
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
376 377 378 379
        flutterProjectFactory = FlutterProjectFactory(
          fileSystem: fs,
          logger: logger,
        );
380 381
      });

382
      void testWithMocks(String description, Future<void> Function() testMethod) {
383 384
        testUsingContext(description, testMethod, overrides: <Type, Generator>{
          FileSystem: () => fs,
385
          ProcessManager: () => FakeProcessManager.any(),
386
          PlistParser: () => testPlistUtils,
387
          XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
388
          FlutterProjectFactory: () => flutterProjectFactory,
389 390 391
        });
      }

392
      testWithMocks('null, if no build settings or plist entries', () async {
393
        final FlutterProject project = await someProject();
394
        expect(await project.ios.productBundleIdentifier(null), isNull);
395
      });
396 397 398

      testWithMocks('from build settings, if no plist', () async {
        final FlutterProject project = await someProject();
399
        project.ios.xcodeProject.createSync();
400 401 402 403 404
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);

405
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
406 407 408
      });

      testWithMocks('from project file, if no plist or build settings', () async {
409
        final FlutterProject project = await someProject();
410 411 412
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
413
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
414
      });
415

416 417
      testWithMocks('from plist, if no variables', () async {
        final FlutterProject project = await someProject();
418
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
419
        testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
420
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
421
      });
422 423

      testWithMocks('from build settings and plist, if default variable', () async {
424
        final FlutterProject project = await someProject();
425
        project.ios.xcodeProject.createSync();
426 427 428 429
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
430
        testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
431

432
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
433
      });
434 435

      testWithMocks('from build settings and plist, by substitution', () async {
436
        final FlutterProject project = await someProject();
437
        project.ios.xcodeProject.createSync();
438
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
439 440 441 442 443
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
          'SUFFIX': 'suffix',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
444
        testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
445

446
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
447
      });
448 449 450

      testWithMocks('fails with no flavor and defined schemes', () async {
        final FlutterProject project = await someProject();
451
        project.ios.xcodeProject.createSync();
452 453
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);

454 455 456 457 458 459 460 461
        await expectToolExitLater(
          project.ios.productBundleIdentifier(null),
          contains('You must specify a --flavor option to select one of the available schemes.')
        );
      });

      testWithMocks('handles case insensitive flavor', () async {
        final FlutterProject project = await someProject();
462
        project.ios.xcodeProject.createSync();
463 464 465 466
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo =XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
467
        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
468

469 470 471 472 473
        expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
      });

      testWithMocks('fails with flavor and default schemes', () async {
        final FlutterProject project = await someProject();
474
        project.ios.xcodeProject.createSync();
475
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
476
        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
477

478 479 480 481 482 483
        await expectToolExitLater(
          project.ios.productBundleIdentifier(buildInfo),
          contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.')
        );
      });

484 485
      testWithMocks('empty surrounded by quotes', () async {
        final FlutterProject project = await someProject();
486 487 488
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('', qualifier: '"');
        });
489
        expect(await project.ios.productBundleIdentifier(null), '');
490
      });
491

492 493
      testWithMocks('surrounded by double quotes', () async {
        final FlutterProject project = await someProject();
494 495 496
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
        });
497
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
498
      });
499

500 501
      testWithMocks('surrounded by single quotes', () async {
        final FlutterProject project = await someProject();
502
        addIosProjectFile(project.directory, projectFileContent: () {
503
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
504
        });
505
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
506
      });
507 508
    });

509 510 511 512
    group('application bundle name', () {
      MemoryFileSystem fs;
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
      setUp(() {
513
        fs = MemoryFileSystem.test();
514 515 516 517 518
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
519
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
520 521 522 523 524 525 526 527
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
528
        project.ios.xcodeProject.createSync();
529 530 531 532
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'FULL_PRODUCT_NAME': 'My App.app'
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
533

534
        expect(await project.ios.hostAppBundleName(null), 'My App.app');
535 536 537 538 539 540 541
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter
      });
    });

542
    group('organization names set', () {
543
      _testInMemory('is empty, if project not created', () async {
544
        final FlutterProject project = await someProject();
545
        expect(await project.organizationNames, isEmpty);
546
      });
547
      _testInMemory('is empty, if no platform folders exist', () async {
548
        final FlutterProject project = await someProject();
549
        project.directory.createSync();
550
        expect(await project.organizationNames, isEmpty);
551
      });
552
      _testInMemory('is populated from iOS bundle identifier', () async {
553
        final FlutterProject project = await someProject();
554
        addIosProjectFile(project.directory, projectFileContent: () {
555
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
556
        });
557
        expect(await project.organizationNames, <String>['io.flutter']);
558
      });
559
      _testInMemory('is populated from Android application ID', () async {
560
        final FlutterProject project = await someProject();
561 562 563 564
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
565
        expect(await project.organizationNames, <String>['io.flutter']);
566
      });
567
      _testInMemory('is populated from iOS bundle identifier in plugin example', () async {
568
        final FlutterProject project = await someProject();
569
        addIosProjectFile(project.example.directory, projectFileContent: () {
570
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
571
        });
572
        expect(await project.organizationNames, <String>['io.flutter']);
573
      });
574
      _testInMemory('is populated from Android application ID in plugin example', () async {
575
        final FlutterProject project = await someProject();
576 577 578 579
        addAndroidGradleFile(project.example.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
580
        expect(await project.organizationNames, <String>['io.flutter']);
581
      });
582
      _testInMemory('is populated from Android group in plugin', () async {
583
        final FlutterProject project = await someProject();
584
        addAndroidWithGroup(project.directory, 'io.flutter.someproject');
585
        expect(await project.organizationNames, <String>['io.flutter']);
586
      });
587
      _testInMemory('is singleton, if sources agree', () async {
588
        final FlutterProject project = await someProject();
589 590 591 592 593 594 595
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.flutter.someproject');
          });
596
        expect(await project.organizationNames, <String>['io.flutter']);
597
      });
598
      _testInMemory('is non-singleton, if sources disagree', () async {
599
        final FlutterProject project = await someProject();
600 601 602 603 604 605 606
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
        addAndroidGradleFile(project.directory,
          gradleFileContent: () {
            return gradleFileWithApplicationId('io.clutter.someproject');
          });
607
        expect(
608
          await project.organizationNames,
609 610 611 612 613
          <String>['io.flutter', 'io.clutter'],
        );
      });
    });
  });
614 615
  group('watch companion', () {
    MemoryFileSystem fs;
616
    FakePlistParser testPlistParser;
617 618 619 620
    MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
    FlutterProjectFactory flutterProjectFactory;
    setUp(() {
      fs = MemoryFileSystem.test();
621
      testPlistParser = FakePlistParser();
622 623 624 625 626 627 628 629 630
      mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: fs,
        logger: logger,
      );
    });

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
631
      expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
632 633 634
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
635
      PlistParser: () => testPlistParser,
636 637 638 639 640 641
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
642 643 644 645
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
646 647 648 649
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
650
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
651 652 653
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
654
        PlistParser: () => testPlistParser,
655 656 657 658 659 660 661 662
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('Info.plist in target does not contain WKCompanionAppBundleIdentifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

663
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
664 665 666
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
667
        PlistParser: () => testPlistParser,
668 669 670 671 672 673 674 675
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('target WKCompanionAppBundleIdentifier is not project bundle identifier', () async {
        final FlutterProject project = await someProject();
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);

676
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
677
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
678 679 680
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
681
        PlistParser: () => testPlistParser,
682 683 684 685 686 687
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion', () async {
        final FlutterProject project = await someProject();
688
        project.ios.xcodeProject.createSync();
689
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
690
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');
691

692
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isTrue);
693 694 695
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
696
        PlistParser: () => testPlistParser,
697 698 699
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717

      testUsingContext('has watch companion with build settings', () async {
        final FlutterProject project = await someProject();
        project.ios.xcodeProject.createSync();
        mockXcodeProjectInterpreter.buildSettings = <String, String>{
          'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
        };
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');

        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isTrue);
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => testPlistParser,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
718 719
    });
  });
720 721
}

722
Future<FlutterProject> someProject() async {
723
  final Directory directory = globals.fs.directory('some_project');
724 725 726 727
  directory.childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
728
  directory.childDirectory('ios').createSync(recursive: true);
729 730 731 732 733 734
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
    .writeAsStringSync('<manifest></manifest>');
735
  return FlutterProject.fromDirectory(directory);
736 737
}

738
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
739
  final Directory directory = globals.fs.directory('plugin_project');
740
  directory.childDirectory('ios').createSync(recursive: true);
741 742
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
743 744 745
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
746 747 748 749 750 751
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
752 753 754 755 756 757 758 759 760 761 762 763
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
764 765
      linux:
        pluginClass: MyPlugin
766 767
      macos:
        pluginClass: MyPlugin
768 769
      windows:
        pluginClass: MyPlugin
770 771 772
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
773
  return FlutterProject.fromDirectory(directory);
774 775
}

776
Future<FlutterProject> aModuleProject() async {
777
  final Directory directory = globals.fs.directory('module_project');
778 779 780 781 782
  directory
    .childDirectory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
783
  directory.childFile('pubspec.yaml').writeAsStringSync('''
784
name: my_module
785
flutter:
786
  module:
787 788 789
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
790
}
791

792 793
/// Executes the [testMethod] in a context where the file system
/// is in memory.
794
@isTest
795
void _testInMemory(String description, Future<void> Function() testMethod) {
796
  Cache.flutterRoot = getFlutterRoot();
797
  final FileSystem testFileSystem = MemoryFileSystem(
798
    style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
799
  );
800 801 802 803 804
  testFileSystem
    .directory('.dart_tool')
    .childFile('package_config.json')
    ..createSync(recursive: true)
    ..writeAsStringSync('{"configVersion":2,"packages":[]}');
805 806
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
807
  final Logger logger = BufferLogger.test();
808 809
  transfer(Cache(
    fileSystem: globals.fs,
810
    logger: logger,
811
    artifacts: <ArtifactSet>[],
812 813 814 815 816 817
    osUtils: OperatingSystemUtils(
      fileSystem: globals.fs,
      logger: logger,
      platform: globals.platform,
      processManager: globals.processManager,
    ),
818 819
    platform: globals.platform,
  ).getArtifactDirectory('gradle_wrapper'), testFileSystem);
820
  transfer(globals.fs.directory(Cache.flutterRoot)
821 822
      .childDirectory('packages')
      .childDirectory('flutter_tools')
823
      .childDirectory('templates'), testFileSystem);
824 825 826 827
  // Set up enough of the packages to satisfy the templating code.
  final File packagesFile = testFileSystem.directory(Cache.flutterRoot)
      .childDirectory('packages')
      .childDirectory('flutter_tools')
828 829
      .childDirectory('.dart_tool')
      .childFile('package_config.json');
830 831 832
  final Directory dummyTemplateImagesDirectory = testFileSystem.directory(Cache.flutterRoot).parent;
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
833 834 835 836 837 838 839 840 841 842 843
  packagesFile.writeAsStringSync(json.encode(<String, Object>{
    'configVersion': 2,
    'packages': <Object>[
      <String, Object>{
        'name': 'flutter_template_images',
        'rootUri': dummyTemplateImagesDirectory.uri.toString(),
        'packageUri': 'lib/',
        'languageVersion': '2.6'
      },
    ],
  }));
844

845 846 847 848
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
849
      FileSystem: () => testFileSystem,
850
      ProcessManager: () => FakeProcessManager.any(),
851 852
      Cache: () => Cache(
        logger: globals.logger,
853
        fileSystem: testFileSystem,
854 855
        osUtils: globals.os,
        platform: globals.platform,
856
        artifacts: <ArtifactSet>[],
857
      ),
858 859 860 861
      FlutterProjectFactory: () => FlutterProjectFactory(
        fileSystem: testFileSystem,
        logger: globals.logger ?? BufferLogger.test(),
      ),
862 863 864 865
    },
  );
}

866 867 868 869 870
/// 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);
871
    for (final FileSystemEntity child in entity.listSync()) {
872 873 874 875 876 877 878 879 880
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
    throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
  }
}

881 882 883 884 885 886 887 888
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

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

889
void addIosProjectFile(Directory directory, {String Function() projectFileContent}) {
890 891 892 893 894
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
895
    ..writeAsStringSync(projectFileContent());
896 897
}

898
void addAndroidGradleFile(Directory directory, { String Function() gradleFileContent }) {
899 900 901 902 903
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
        ..createSync(recursive: true)
904
        ..writeAsStringSync(gradleFileContent());
905 906 907 908 909 910 911 912
}

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

913 914 915 916 917
String get validPubspec => '''
name: hello
flutter:
''';

918 919 920 921 922 923 924 925 926 927
String get validPubspecWithDependencies => '''
name: hello
flutter:

dependencies:
  plugin_a:
  plugin_b:
''';


928 929 930 931 932 933
String get invalidPubspec => '''
name: hello
flutter:
  invalid:
''';

934 935 936 937 938 939 940 941
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

942
String projectFileWithBundleId(String id, {String qualifier}) {
943 944 945 946 947
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
948
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
949 950 951 952 953 954 955 956 957 958 959
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
960
    compileSdkVersion 30
961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976

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

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

apply plugin: 'com.android.library'

android {
977
    compileSdkVersion 30
978 979 980
}
''';
}
981 982 983 984 985 986 987 988 989 990

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

992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
class MockXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
  Map<String, String> buildSettings = <String, String>{};
  XcodeProjectInfo xcodeProjectInfo;

  @override
  Future<Map<String, String>> getBuildSettings(String projectPath, {
    XcodeProjectBuildContext buildContext,
    Duration timeout = const Duration(minutes: 1),
  }) async {
    return buildSettings;
  }

  @override
  Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
    return xcodeProjectInfo;
  }

1009 1010 1011
  @override
  bool get isInstalled => true;
}