project_test.dart 38.5 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
import 'package:file/file.dart';
import 'package:file/memory.dart';
7
import 'package:flutter_tools/src/base/context.dart';
8
import 'package:flutter_tools/src/base/file_system.dart';
9
import 'package:flutter_tools/src/base/logger.dart';
10
import 'package:flutter_tools/src/build_info.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/convert.dart';
13
import 'package:flutter_tools/src/features.dart';
14
import 'package:flutter_tools/src/flutter_manifest.dart';
15
import 'package:flutter_tools/src/ios/plist_parser.dart';
16
import 'package:flutter_tools/src/ios/xcodeproj.dart';
17
import 'package:flutter_tools/src/project.dart';
18
import 'package:flutter_tools/src/globals.dart' as globals;
19
import 'package:meta/meta.dart';
20
import 'package:mockito/mockito.dart';
21

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

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

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

40
      _testInMemory('fails on invalid pubspec.yaml', () async {
41
        final Directory directory = globals.fs.directory('myproject');
42 43 44
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
45 46 47

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
48
          throwsToolExit(),
49 50 51
        );
      });

52
      _testInMemory('fails on pubspec.yaml parse failure', () async {
53
        final Directory directory = globals.fs.directory('myproject');
54 55 56 57 58 59
        directory.childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(parseErrorPubspec);

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
60
          throwsToolExit(),
61 62 63
        );
      });

64
      _testInMemory('fails on invalid example/pubspec.yaml', () async {
65
        final Directory directory = globals.fs.directory('myproject');
66 67 68
        directory.childDirectory('example').childFile('pubspec.yaml')
          ..createSync(recursive: true)
          ..writeAsStringSync(invalidPubspec);
69 70 71

        expect(
          () => FlutterProject.fromDirectory(directory),
Dan Field's avatar
Dan Field committed
72
          throwsToolExit(),
73 74 75
        );
      });

76
      _testInMemory('treats missing pubspec.yaml as empty', () async {
77
        final Directory directory = globals.fs.directory('myproject')
78
          ..createSync(recursive: true);
79
        expect((FlutterProject.fromDirectory(directory)).manifest.isEmpty,
80 81 82 83
          true,
        );
      });

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

95
      _testInMemory('sets up location', () async {
96
        final Directory directory = globals.fs.directory('myproject');
97
        expect(
98
          FlutterProject.fromDirectory(directory).directory.absolute.path,
99 100 101
          directory.absolute.path,
        );
        expect(
102
          FlutterProject.fromPath(directory.path).directory.absolute.path,
103 104 105
          directory.absolute.path,
        );
        expect(
106
          FlutterProject.current().directory.absolute.path,
107
          globals.fs.currentDirectory.absolute.path,
108 109
        );
      });
110
    });
111

112
    group('ensure ready for platform-specific tooling', () {
113
      _testInMemory('does nothing, if project is not created', () async {
114
        final FlutterProject project = FlutterProject(
115
          globals.fs.directory('not_created'),
116 117
          FlutterManifest.empty(logger: logger),
          FlutterManifest.empty(logger: logger),
118
        );
119
        await project.ensureReadyForPlatformSpecificTooling();
120
        expectNotExists(project.directory);
121
      });
122
      _testInMemory('does nothing in plugin or package root project', () async {
123
        final FlutterProject project = await aPluginProject();
124
        await project.ensureReadyForPlatformSpecificTooling();
125
        expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
126
        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
127
        expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
128
        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
129
      });
130
      _testInMemory('injects plugins for iOS', () async {
131
        final FlutterProject project = await someProject();
132
        await project.ensureReadyForPlatformSpecificTooling();
133
        expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
134
      });
135
      _testInMemory('generates Xcode configuration for iOS', () async {
136
        final FlutterProject project = await someProject();
137
        await project.ensureReadyForPlatformSpecificTooling();
138
        expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
139
      });
140
      _testInMemory('injects plugins for Android', () async {
141
        final FlutterProject project = await someProject();
142
        await project.ensureReadyForPlatformSpecificTooling();
143
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
144
      });
145
      _testInMemory('updates local properties for Android', () async {
146
        final FlutterProject project = await someProject();
147
        await project.ensureReadyForPlatformSpecificTooling();
148
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
149
      });
150 151 152 153 154 155 156 157 158 159 160 161 162 163
      _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" />.

        await project.ensureReadyForPlatformSpecificTooling();
        expect(testLogger.statusText, contains('https://flutter.dev/go/android-project-migration'));
      });
      _testInMemory('updates local properties for Android', () async {
        final FlutterProject project = await someProject();
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
      });
164 165 166 167 168 169
      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>{
170
        FileSystem: () => MemoryFileSystem(),
171
        ProcessManager: () => FakeProcessManager.any(),
172
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
173 174 175 176
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
177 178 179 180 181 182 183
      });
      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>{
184
        FileSystem: () => MemoryFileSystem(),
185
        ProcessManager: () => FakeProcessManager.any(),
186
        FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
187 188 189 190
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
191 192 193
      });
      testUsingContext('injects plugins for Linux', () async {
        final FlutterProject project = await someProject();
194
        project.linux.cmakeFile.createSync(recursive: true);
195 196 197 198
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
199
        FileSystem: () => MemoryFileSystem(),
200
        ProcessManager: () => FakeProcessManager.any(),
201
        FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
202 203 204 205
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
206 207 208
      });
      testUsingContext('injects plugins for Windows', () async {
        final FlutterProject project = await someProject();
209
        project.windows.cmakeFile.createSync(recursive: true);
210 211 212 213
        await project.ensureReadyForPlatformSpecificTooling();
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
        expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
      }, overrides: <Type, Generator>{
214
        FileSystem: () => MemoryFileSystem(),
215
        ProcessManager: () => FakeProcessManager.any(),
216
        FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
217 218 219 220
        FlutterProjectFactory: () => FlutterProjectFactory(
          logger: logger,
          fileSystem: globals.fs,
        ),
221
      });
222
      _testInMemory('creates Android library in module', () async {
223
        final FlutterProject project = await aModuleProject();
224
        await project.ensureReadyForPlatformSpecificTooling();
225 226 227
        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
228
      });
229
      _testInMemory('creates iOS pod in module', () async {
230
        final FlutterProject project = await aModuleProject();
231
        await project.ensureReadyForPlatformSpecificTooling();
232
        final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
233
        expectExists(flutter.childFile('podhelper.rb'));
234 235
        expectExists(flutter.childFile('flutter_export_environment.sh'));
        expectExists(flutter.childFile('${project.manifest.appName}.podspec'));
236 237 238 239 240 241
        expectExists(flutter.childFile('Generated.xcconfig'));
        final Directory pluginRegistrantClasses = flutter
            .childDirectory('FlutterPluginRegistrant')
            .childDirectory('Classes');
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
        expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
242
      });
243 244 245 246 247 248 249 250 251 252 253 254 255

      testUsingContext('Version.json info is correct', (){
        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);
        final dynamic versionInfo = jsonDecode(project.getVersionInfo());
        expect(versionInfo['app_name'],'test');
        expect(versionInfo['version'],'1.0.0');
        expect(versionInfo['build_number'],'3');
      });
256 257
    });

258
    group('module status', () {
259
      _testInMemory('is known for module', () async {
260 261 262 263
        final FlutterProject project = await aModuleProject();
        expect(project.isModule, isTrue);
        expect(project.android.isModule, isTrue);
        expect(project.ios.isModule, isTrue);
264
        expect(project.android.hostAppGradleRoot.basename, '.android');
265
        expect(project.ios.hostAppRoot.basename, '.ios');
266
      });
267
      _testInMemory('is known for non-module', () async {
268
        final FlutterProject project = await someProject();
269 270 271
        expect(project.isModule, isFalse);
        expect(project.android.isModule, isFalse);
        expect(project.ios.isModule, isFalse);
272
        expect(project.android.hostAppGradleRoot.basename, 'android');
273
        expect(project.ios.hostAppRoot.basename, 'ios');
274
      });
275
    });
276 277

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

292 293 294
    group('language', () {
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
      MemoryFileSystem fs;
295
      FlutterProjectFactory flutterProjectFactory;
296 297 298
      setUp(() {
        fs = MemoryFileSystem();
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
299 300 301 302
        flutterProjectFactory = FlutterProjectFactory(
          logger: logger,
          fileSystem: fs,
        );
303 304
      });

305
      _testInMemory('default host app language', () async {
306 307 308 309
        final FlutterProject project = await someProject();
        expect(project.android.isKotlin, isFalse);
      });

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

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

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

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

354
      testWithMocks('null, if no build settings or plist entries', () async {
355
        final FlutterProject project = await someProject();
356
        expect(await project.ios.productBundleIdentifier(null), isNull);
357
      });
358 359 360

      testWithMocks('from build settings, if no plist', () async {
        final FlutterProject project = await someProject();
361
        project.ios.xcodeProject.createSync();
362
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
363 364 365 366 367 368
                (_) {
              return Future<Map<String,String>>.value(<String, String>{
                'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              });
            }
        );
369 370 371
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });
372
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
373 374 375
      });

      testWithMocks('from project file, if no plist or build settings', () async {
376
        final FlutterProject project = await someProject();
377 378 379
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject');
        });
380
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
381
      });
382

383 384
      testWithMocks('from plist, if no variables', () async {
        final FlutterProject project = await someProject();
385
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
386
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject');
387
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
388
      });
389 390

      testWithMocks('from build settings and plist, if default variable', () async {
391
        final FlutterProject project = await someProject();
392
        project.ios.xcodeProject.createSync();
393
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
394 395 396 397 398 399
                (_) {
              return Future<Map<String,String>>.value(<String, String>{
                'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              });
            }
        );
400 401 402 403
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });

404
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn(r'$(PRODUCT_BUNDLE_IDENTIFIER)');
405
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
406
      });
407 408

      testWithMocks('from build settings and plist, by substitution', () async {
409
        final FlutterProject project = await someProject();
410
        project.ios.xcodeProject.createSync();
411
        project.ios.defaultHostInfoPlist.createSync(recursive: true);
412
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
413 414 415 416 417 418 419
          (_) {
            return Future<Map<String,String>>.value(<String, String>{
              'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              'SUFFIX': 'suffix',
            });
          }
        );
420 421 422 423
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });

424
        when(mockPlistUtils.getValueFromFile(any, any)).thenReturn(r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
425
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
426
      });
427 428 429

      testWithMocks('fails with no flavor and defined schemes', () async {
        final FlutterProject project = await someProject();
430
        project.ios.xcodeProject.createSync();
431 432 433 434 435 436 437 438 439 440 441
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger));
        });
        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();
442
        project.ios.xcodeProject.createSync();
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
                (_) {
              return Future<Map<String,String>>.value(<String, String>{
                'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
              });
            }
        );
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger));
        });

        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
        expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
      });

      testWithMocks('fails with flavor and default schemes', () async {
        final FlutterProject project = await someProject();
460
        project.ios.xcodeProject.createSync();
461 462 463 464 465 466 467 468 469 470 471
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });

        const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
        await expectToolExitLater(
          project.ios.productBundleIdentifier(buildInfo),
          contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.')
        );
      });

472 473
      testWithMocks('empty surrounded by quotes', () async {
        final FlutterProject project = await someProject();
474 475 476
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('', qualifier: '"');
        });
477
        expect(await project.ios.productBundleIdentifier(null), '');
478 479 480
      });
      testWithMocks('surrounded by double quotes', () async {
        final FlutterProject project = await someProject();
481 482 483
        addIosProjectFile(project.directory, projectFileContent: () {
          return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
        });
484
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
485 486 487
      });
      testWithMocks('surrounded by single quotes', () async {
        final FlutterProject project = await someProject();
488
        addIosProjectFile(project.directory, projectFileContent: () {
489
          return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
490
        });
491
        expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
492
      });
493 494
    });

495 496 497 498 499 500 501 502 503 504
    group('application bundle name', () {
      MemoryFileSystem fs;
      MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
      setUp(() {
        fs = MemoryFileSystem();
        mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
      });

      testUsingContext('app product name defaults to Runner.app', () async {
        final FlutterProject project = await someProject();
505
        expect(await project.ios.hostAppBundleName(null), 'Runner.app');
506 507 508 509 510 511 512 513
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter
      });

      testUsingContext('app product name xcodebuild settings', () async {
        final FlutterProject project = await someProject();
514
        project.ios.xcodeProject.createSync();
515
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer((_) {
516 517 518 519
          return Future<Map<String,String>>.value(<String, String>{
            'FULL_PRODUCT_NAME': 'My App.app'
          });
        });
520 521 522
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });
523

524
        expect(await project.ios.hostAppBundleName(null), 'My App.app');
525 526 527 528 529 530 531
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter
      });
    });

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

    testUsingContext('cannot find bundle identifier', () async {
      final FlutterProject project = await someProject();
621
      expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
622 623 624 625 626 627 628 629 630 631
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
      PlistParser: () => mockPlistUtils,
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
      FlutterProjectFactory: () => flutterProjectFactory,
    });

    group('with bundle identifier', () {
      setUp(() {
632
        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
633 634 635 636 637 638
            (_) {
            return Future<Map<String,String>>.value(<String, String>{
              'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
            });
          }
        );
639 640 641
        when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer( (_) {
          return Future<XcodeProjectInfo>.value(XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger));
        });
642 643 644 645
      });

      testUsingContext('no Info.plist in target', () async {
        final FlutterProject project = await someProject();
646
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
647 648 649 650 651 652 653 654 655 656 657 658
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => mockPlistUtils,
        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);

659
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
660 661 662 663 664 665 666 667 668 669 670 671 672
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => mockPlistUtils,
        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);

        when(mockPlistUtils.getValueFromFile(any, 'WKCompanionAppBundleIdentifier')).thenReturn('io.flutter.someOTHERproject');
673
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isFalse);
674 675 676 677 678 679 680 681 682 683
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => mockPlistUtils,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });

      testUsingContext('has watch companion', () async {
        final FlutterProject project = await someProject();
684
        project.ios.xcodeProject.createSync();
685 686 687
        project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
        when(mockPlistUtils.getValueFromFile(any, 'WKCompanionAppBundleIdentifier')).thenReturn('io.flutter.someProject');

688
        expect(await project.ios.containsWatchCompanion(<String>['WatchTarget'], null), isTrue);
689 690 691 692 693 694 695 696 697
      }, overrides: <Type, Generator>{
        FileSystem: () => fs,
        ProcessManager: () => FakeProcessManager.any(),
        PlistParser: () => mockPlistUtils,
        XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
        FlutterProjectFactory: () => flutterProjectFactory,
      });
    });
  });
698 699
}

700
Future<FlutterProject> someProject() async {
701
  final Directory directory = globals.fs.directory('some_project');
702 703
  directory.childFile('.packages').createSync(recursive: true);
  directory.childDirectory('ios').createSync(recursive: true);
704 705 706 707 708 709
  final Directory androidDirectory = directory
      .childDirectory('android')
      ..createSync(recursive: true);
  androidDirectory
    .childFile('AndroidManifest.xml')
    .writeAsStringSync('<manifest></manifest>');
710
  return FlutterProject.fromDirectory(directory);
711 712
}

713
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
714
  final Directory directory = globals.fs.directory('plugin_project');
715
  directory.childDirectory('ios').createSync(recursive: true);
716 717
  directory.childDirectory('android').createSync(recursive: true);
  directory.childDirectory('example').createSync(recursive: true);
718 719 720
  String pluginPubSpec;
  if (legacy) {
    pluginPubSpec = '''
721 722 723 724 725 726
name: my_plugin
flutter:
  plugin:
    androidPackage: com.example
    pluginClass: MyPlugin
    iosPrefix: FLT
727 728 729 730 731 732 733 734 735 736 737 738
''';
  } else {
    pluginPubSpec = '''
name: my_plugin
flutter:
  plugin:
    platforms:
      android:
        package: com.example
        pluginClass: MyPlugin
      ios:
        pluginClass: MyPlugin
739 740
      linux:
        pluginClass: MyPlugin
741 742
      macos:
        pluginClass: MyPlugin
743 744
      windows:
        pluginClass: MyPlugin
745 746 747
''';
  }
  directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
748
  return FlutterProject.fromDirectory(directory);
749 750
}

751
Future<FlutterProject> aModuleProject() async {
752
  final Directory directory = globals.fs.directory('module_project');
753
  directory.childFile('.packages').createSync(recursive: true);
754
  directory.childFile('pubspec.yaml').writeAsStringSync('''
755
name: my_module
756
flutter:
757
  module:
758 759 760
    androidPackage: com.example
''');
  return FlutterProject.fromDirectory(directory);
761
}
762

763 764
/// Executes the [testMethod] in a context where the file system
/// is in memory.
765
@isTest
766
void _testInMemory(String description, Future<void> testMethod()) {
767
  Cache.flutterRoot = getFlutterRoot();
768
  final FileSystem testFileSystem = MemoryFileSystem(
769
    style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
770
  );
771
  testFileSystem.file('.packages').writeAsStringSync('\n');
772 773
  // Transfer needed parts of the Flutter installation folder
  // to the in-memory file system used during testing.
774
  transfer(Cache().getArtifactDirectory('gradle_wrapper'), testFileSystem);
775
  transfer(globals.fs.directory(Cache.flutterRoot)
776 777
      .childDirectory('packages')
      .childDirectory('flutter_tools')
778
      .childDirectory('templates'), testFileSystem);
779
  transfer(globals.fs.directory(Cache.flutterRoot)
780 781
      .childDirectory('packages')
      .childDirectory('flutter_tools')
782
      .childDirectory('schema'), testFileSystem);
783 784 785 786 787 788 789 790 791
  // Set up enough of the packages to satisfy the templating code.
  final File packagesFile = testFileSystem.directory(Cache.flutterRoot)
      .childDirectory('packages')
      .childDirectory('flutter_tools')
      .childFile('.packages');
  final Directory dummyTemplateImagesDirectory = testFileSystem.directory(Cache.flutterRoot).parent;
  dummyTemplateImagesDirectory.createSync(recursive: true);
  packagesFile.createSync(recursive: true);
  packagesFile.writeAsStringSync('flutter_template_images:${dummyTemplateImagesDirectory.uri}');
792

793 794 795 796
  final FlutterProjectFactory flutterProjectFactory = FlutterProjectFactory(
    fileSystem: testFileSystem,
    logger: globals.logger ?? BufferLogger.test(),
  );
797

798 799 800 801
  testUsingContext(
    description,
    testMethod,
    overrides: <Type, Generator>{
802
      FileSystem: () => testFileSystem,
803
      ProcessManager: () => FakeProcessManager.any(),
804
      Cache: () => Cache(),
805
      FlutterProjectFactory: () => flutterProjectFactory,
806 807 808 809
    },
  );
}

810 811 812 813 814
/// 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);
815
    for (final FileSystemEntity child in entity.listSync()) {
816 817 818 819 820 821 822 823 824
      transfer(child, target);
    }
  } else if (entity is File) {
    target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
  } else {
    throw 'Unsupported FileSystemEntity ${entity.runtimeType}';
  }
}

825 826 827 828 829 830 831 832
void expectExists(FileSystemEntity entity) {
  expect(entity.existsSync(), isTrue);
}

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

833
void addIosProjectFile(Directory directory, {String projectFileContent()}) {
834 835 836 837 838
  directory
      .childDirectory('ios')
      .childDirectory('Runner.xcodeproj')
      .childFile('project.pbxproj')
        ..createSync(recursive: true)
839
    ..writeAsStringSync(projectFileContent());
840 841
}

842
void addAndroidGradleFile(Directory directory, { String gradleFileContent() }) {
843 844 845 846 847
  directory
      .childDirectory('android')
      .childDirectory('app')
      .childFile('build.gradle')
        ..createSync(recursive: true)
848
        ..writeAsStringSync(gradleFileContent());
849 850 851 852 853 854 855 856
}

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

857 858 859 860 861 862 863 864 865 866 867
String get validPubspec => '''
name: hello
flutter:
''';

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

868 869 870 871 872 873 874 875
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
    something:
  something_else:
''';

876
String projectFileWithBundleId(String id, {String qualifier}) {
877 878 879 880 881
  return '''
97C147061CF9000F007C117D /* Debug */ = {
  isa = XCBuildConfiguration;
  baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
  buildSettings = {
882
    PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
883 884 885 886 887 888 889 890 891 892 893
    PRODUCT_NAME = "\$(TARGET_NAME)";
  };
  name = Debug;
};
''';
}

String gradleFileWithApplicationId(String id) {
  return '''
apply plugin: 'com.android.application'
android {
894
    compileSdkVersion 29
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910

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

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

apply plugin: 'com.android.library'

android {
911
    compileSdkVersion 29
912 913 914
}
''';
}
915 916 917 918 919 920 921 922 923 924

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

926
class MockPlistUtils extends Mock implements PlistParser {}
927 928 929 930 931

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